Компилятор Java выбирает неправильную перегрузку
@Test
public void test() {
MyProperties props = new MyProperties();
props.setProperty("value", new Date());
StringUtils.isNullOrEmpty(props.getProperty("value"));
}
public class MyProperties {
private Map<String, Object> properties = new HashMap<String, Object>();
public void setProperty(String name, Object value) {
properties.put(name, value);
}
@SuppressWarnings("unchecked")
public <T> T getProperty(String name) {
return (T) properties.get(name);
}
}
public class StringUtils {
public static boolean isNullOrEmpty(Object string) {
return isNullOrEmpty(valueOf(string));
}
public static String valueOf(Object string) {
if (string == null) {
return "";
}
return string.toString();
}
public static boolean isNullOrEmpty(String string) {
if (string == null || string.length() == 0) {
return false;
}
int strLength = string.length();
for (int i = 0; i < strLength; i++) {
char charAt = string.charAt(i);
if (charAt > ' ') {
return true;
}
}
return false;
}
}
В течение многих лет этот unit test прошел. Затем после перехода на Java 8 в определенных средах, когда код скомпилирован через javac, он выбирает перегрузку StringUtils.isNullOrEmpty(String). Это приведет к сбою unit test со следующим сообщением об ошибке:
java.lang.ClassCastException: java.util.Date cannot be cast to java.lang.String
at com.foo.bar.StringUtils_UT.test(StringUtils_UT.java:35)
Тестирование модулей проходит при компиляции и запуске на моем компьютере через ant (ant 1.9.6, jdk_8_u60, Windows 7 64 бит), но с ошибкой выполняется с теми же версиями ant и java (ant 1.9.6 jdk_8_u60, Ubuntu 12.04.4 32bit).
Java вывод типа, который выбирает наиболее специфическую перегрузку из всех применимых перегрузок при компиляции, был изменен в Java 8. Я полагаю, что моя проблема имеет какое-то отношение к это.
Я знаю, что компилятор видит возвращаемый тип метода MyProperties.getProperty(...) как T, а не Date. Поскольку компилятор не знает тип возвращаемого метода getProperty (...), почему он выбирает StringUtils.isNullorEmpty(String) вместо StringUtils.isNullorEmpty(Object) - который всегда должен работать?
Является ли это ошибкой на Java или просто результатом изменения вывода типа Java 8? Кроме того, почему разные среды, использующие одну и ту же версию java, компилируют этот код по-другому?
Ответы
Ответ 1
Этот код пахнет. Да, это проходит под Java 7, и да, это хорошо работает с Java 7, но здесь есть что-то определенно.
Сначала расскажите об этом родовом типе.
@SuppressWarnings("unchecked")
public <T> T getProperty(String name) {
return (T) properties.get(name);
}
Можете ли вы с первого взгляда сделать вывод, что должно быть T
? Если я запустил эти приведения в режиме соответствия Java 7 с помощью IntelliJ в этой точной строке, я верну эту очень полезную ClassCastException
:
Cannot cast java.util.Date to T
Итак, это означает, что на каком-то уровне Java знал, что здесь что-то есть, но он решил изменить этот листинг вместо (T)
на (Object)
.
@SuppressWarnings("unchecked")
public <T> Object getProperty(String name) {
return (Object) properties.get(name);
}
В этом случае бросок избыточен, и вы возвращаете Object
с карты, как и следовало ожидать. Затем вызывается правильная перегрузка.
Теперь, в Java 8, все немного более разумно; так как вы действительно не предоставляете тип методу getProperty
, он взрывается, так как он действительно не может отличить java.util.Date
до T
.
В конечном счете, я замалчиваю главное:
Это использование дженериков нарушено и неверно.
Вам даже не нужны дженерики. Ваш код может обрабатывать либо String
, либо Object
, и ваша карта содержит только Object
.
Вы должны только возвращать Object
из метода getProperty
, так как это то, что вы можете только вернуться с карты в любом случае.
public Object getProperty(String name) {
return properties.get(name);
}
Это означает, что вы больше не получаете возможность напрямую обращаться к методу с сигнатурой String
(поскольку вы передаете Object
сейчас), но это означает, что ваш сломанный код генериков может наконец, покоятся.
Если вы действительно хотите сохранить это поведение, вам нужно будет ввести новый параметр в свою функцию, который фактически позволил вам указать, какой тип объекта вы хотели бы вернуть с вашей карты.
@SuppressWarnings("unchecked")
public <T> T getProperty(String name, Class<T> clazz) {
return (T) properties.get(name);
}
Затем вы можете вызвать свой метод таким образом:
StringUtils.isNullOrEmpty(props.getProperty("value", Date.class));
Теперь мы абсолютно уверены в том, что такое T
, а Java 8 доволен этим кодом. Это все еще немного запах, так как вы храните вещи в Map<String, Object>
; если у вас есть переопределенный метод Object
, и вы можете гарантировать, что все объекты на этой карте имеют значимый toString
, тогда я лично избегу этого кода.
Ответ 2
Java 8 улучшил вывод целевого типа. Это означает, что компилятор будет использовать целевой тип для вывода параметра типа.
В вашем случае это означает, что в этом утверждении
StringUtils.isNullOrEmpty(props.getProperty("value"));
Java будет использовать тип параметра isNullOrEmpty
для определения параметра типа метода getProperty
. Но есть 2 перегрузки isNullOrEmpty
, один принимает Object
и один принимает a String
. На T
нет ограничений, поэтому компилятор выберет наиболее подходящий метод, который соответствует - перегрузка, которая принимает String
. T
определяется как String
.
Ваш тэг T
не установлен, поэтому компилятор разрешает его, но он дает вам предупреждение без предупреждения о том, чтобы отличить Object
до T
. Однако, когда вызывается метод isNullOrEmpty
, возникает исключение класса cast, потому что исходный объект действительно был Date
, который не может быть преобразован в String
.
Это иллюстрирует опасность игнорирования предупреждения без предупреждения.
Это не произошло в Java 7, потому что улучшенный вывод типа цели не существовал. Компилятор сделал вывод Object
.
Улучшенный вывод целевого типа в Java 8 показал, что ваш метод getProperty
неправильно игнорирует предупреждение о немедленном предупреждении, которое вы подавляете с помощью @SuppressWarnings
.
Чтобы исправить это, даже не имеет перегруженного метода, который принимает String
. Переместите String
-специфическую логику внутри перегрузки, которая принимает Object
.
public static boolean isNullOrEmpty(Object o) {
// null instanceof String is false
String string = (o instanceof String) ? ((String) o) : valueOf(o);
if (string == null || string.length() == 0) {
return false;
}
int strLength = string.length();
for (int i = 0; i < strLength; i++) {
char charAt = string.charAt(i);
if (charAt > ' ') {
return true;
}
}
return false;
}
Конечно, это означает, что дженерики по методу getProperty
не имеют смысла. Удалите их.
public Object getProperty(String name) {
return properties.get(name);
}