Я множество раз слышал о том, что дизайн Generic типов в Java является неудачным. По большей части претензии сводятся к отсутствию поддержки примитивных типов (которую планируют добавить) и к стиранию типов, а конкретнее — невозможности получить фактический тип параметра в рантайме. Лично я не считаю стирание типов проблемой, как и дизайн Generic-ов плохим. Но есть моменты, которые меня порядком раздражают, но при этом не так часто упоминаются.
1
Например, мы знаем, что метод Class#getAnnotation
параметризован и имеет следующую сигнатуру: public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
. Значит, можно писать вот такой код:
Deprecated d = Object.class.getAnnotation(Deprecated.class);
Тут я решаю вынести Object.class
в отдельную переменную и код перестаёт компилироваться:
Class clazz = Object.class; // incompatible types: // java.lang.annotation.Annotation cannot be converted to java.lang.Deprecated Deprecated d = clazz.getAnnotation(Deprecated.class);
Где я ошибся?
Ошибся я в том, что не параметризовал тип переменной clazz
.
Получается, что стирание у типа Class
так же стирает типы во всех его методах! Зачем так было делать — понятия не имею. Вносим минимальное исправление в код и всё работает как надо.
Class<Object> clazz = Object.class; Deprecated d = clazz.getAnnotation(Deprecated.class);
2
Второй пример немного надуманный, но показательный. Представьте, есть у вас такой простой класс:
class Ref<T> { private T value = null; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
Имея переменную ref
я решу написать такой код. Что с ним может произойти плохого?
ref.setValue(ref.getValue());
Разумно было бы считать, что он всегда скомпилируется, но это не так! Вам всего лишь стоит объявить переменную ref
с типом Ref<?>
и вы получите ошибку incompatible types: java.lang.Object cannot be converted to capture#1 of ?
То есть wildcard в геттере автоматически раскрывается в Object
. Мы не можем полагаться на то, что в наших выражениях он будет равен самому себе. Выстрелить себе в ногу этим пусть и сложно, но точно можно.
3
Далее, пусть есть такая очень простая иерархия классов:
class HasList<T extends List> { protected final T list; public HasList(T list) { this.list = list; } public T getList() { return list; } } class HasArrayList<T extends ArrayList> extends HasList<T> { public HasArrayList(T list) { super(list); } }
Напишем следующий код:
HasArrayList<?> h = new HasArrayList<>(new ArrayList<>()); ArrayList list = h.getList();
Параметр T
класса HasArrayList
имеет верхнюю границу равную ArrayList
, а значит при стирании типов код всё ещё должен компилироваться.
HasArrayList h = new HasArrayList<>(new ArrayList<>()); // incompatible types: java.util.List cannot be converted to java.util.ArrayList ArrayList list = h.getList();
Ну вот, опять не работает. Сейчас то что не так?
Не так то, что в сигнатуре метода getList
возвращаемым типом является List
, а компилятору просто лень расставлять явные приведения типов. Исправляется всё очень просто — надо переопределить данный метод в подклассе.
class HasArrayList<T extends ArrayList> extends HasList<T> { public HasArrayList(T list) { super(list); } @Override public T getList() { return super.getList(); } }
При этом компилятор сгенерирует синтетический bridge метод, возвращающий ArrayList
, и именно он и будет вызван. Очевидно же…
Вообще, если у класса есть тип-параметр, то лучше в коде его не игнорировать, в крайнем случае можно указать <?>
. Иначе компилятор наделает подобной ерунды, а нам потом нервничать.
4
Последняя ситуация наиболее хитрая. Решил я написать подобный класс:
class MyArrayList<T extends Cloneable & BooleanSupplier> extends ArrayList<T> { public void removeTrueElements() { this.removeIf(BooleanSupplier::getAsBoolean); } }
Как вы считаете, будут ли при вызове этого метода какие-нибудь проблемы?
new MyArrayList<>().removeTrueElements();
Может показаться смешным, но этот код при запуске выбросит исключение:
Exception in thread "main" java.lang.BootstrapMethodError: call site initialization exception ... Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type interface java.lang.Cloneable; not a subtype of implementation type interface java.util.function.BooleanSupplier ...
Хотя нет, я вас обманул. Этот код будет работать, если компилировать его из JDK9, а вот компилятор JDK8 допускает на нём ошибку.
Да, всё именно так, это ошибка не рантайма, а компилятора. Почему нельзя предупредить при компиляции, что генерация этой лямбды упадёт, я не понимаю.
Как исправить код без перехода на Java 9? Вот так:
public void removeTrueElements() { this.removeIf(t -> t.getAsBoolean()); }
Ну и откомментировать, конечно, чтобы ваш коллега не сконвертировал всё обратно в method reference.
Вместо заключения
Уверен, у многих из вас тоже встречались ситуации, когда поведение дженериков вызывало недоумение.
Делитесь в комментариях, обсудим.
ссылка на оригинал статьи https://habrahabr.ru/post/329550/
Добавить комментарий