Общая верхняя граница возвращаемого типа - интерфейс против класса - удивительно действительный код
Это реальный пример из API сторонней библиотеки, но упрощен.
Скомпилирован с Oracle JDK 8u72
Рассмотрим эти два метода:
<X extends CharSequence> X getCharSequence() {
return (X) "hello";
}
<X extends String> X getString() {
return (X) "hello";
}
Оба сообщают о предупреждении "непроверенного броска" - я понимаю почему. Меня беспокоит то, что я могу назвать
Integer x = getCharSequence();
и он компилируется? Компилятор должен знать, что Integer
не реализует CharSequence
. Вызов
Integer y = getString();
дает ошибку (как и ожидалось)
incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String
Может кто-нибудь объяснить, почему это поведение считается действительным? Как это было бы полезно?
Клиент не знает, что этот вызов небезопасен - код клиента компилируется без предупреждения. Почему компилятор не предупредил об этом/не допустил ошибку?
Кроме того, как это отличается от этого примера:
<X extends CharSequence> void doCharSequence(List<X> l) {
}
List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles
List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error
Попытка пройти List<Integer>
дает ошибку, как и ожидалось:
method doCharSequence in class generic.GenericTest cannot be applied to given types;
required: java.util.List<X>
found: java.util.List<java.lang.Integer>
reason: inference variable X has incompatible bounds
equality constraints: java.lang.Integer
upper bounds: java.lang.CharSequence
Если это сообщается как ошибка, почему Integer x = getCharSequence();
не является?
Ответы
Ответ 1
CharSequence
является interface
. Поэтому даже если SomeClass
не реализует CharSequence
, было бы вполне возможно создать класс
class SubClass extends SomeClass implements CharSequence
Поэтому вы можете написать
SomeClass c = getCharSequence();
потому что предполагаемый тип X
- это тип пересечения SomeClass & CharSequence
.
Это немного странно в случае Integer
, потому что Integer
является окончательным, но final
не играет никакой роли в этих правилах. Например, вы можете написать
<T extends Integer & CharSequence>
С другой стороны, String
не является interface
, поэтому невозможно было бы расширить SomeClass
, чтобы получить подтип String
, потому что java не поддерживает множественное наследование для классов.
С примером List
вам нужно помнить, что дженерики не являются ковариантными или контравариантными. Это означает, что если X
является подтипом Y
, List<X>
не является ни подтипом, ни супертипом List<Y>
. Поскольку Integer
не реализует CharSequence
, вы не можете использовать List<Integer>
в своем методе doCharSequence
.
Вы можете, однако, получить это, чтобы скомпилировать
<T extends Integer & CharSequence> void foo(List<T> list) {
doCharSequence(list);
}
Если у вас есть метод, который возвращает List<T>
следующим образом:
static <T extends CharSequence> List<T> foo()
вы можете сделать
List<? extends Integer> list = foo();
Опять же, это потому, что выводимый тип Integer & CharSequence
, и это подтип Integer
.
Типы пересечений возникают неявно, когда вы указываете несколько границ (например, <T extends SomeClass & CharSequence>
).
Для получения дополнительной информации здесь является частью JLS, где объясняется, как работают ограничения типов. Вы можете включить несколько интерфейсов, например.
<T extends String & CharSequence & List & Comparator>
но только первая оценка может быть неинтерфейсом.
Ответ 2
Тип, который выводится вашим компилятором до назначения для X
, это Integer & CharSequence
. Этот тип кажется странным, потому что Integer
является окончательным, но он является вполне допустимым типом в Java. Затем он преобразуется в Integer
, что отлично.
Существует только одно возможное значение для типа Integer & CharSequence
: null
. Со следующей реализацией:
<X extends CharSequence> X getCharSequence() {
return null;
}
Следующее назначение будет работать:
Integer x = getCharSequence();
Из-за этого возможного значения нет причин, по которым назначение должно быть неправильным, даже если оно явно бесполезно. Предупреждение было бы полезно.
Реальная проблема - это API, а не сайт вызова
На самом деле, я недавно писал об этом антивирусном шаблоне API. Вы должны (почти) никогда не разрабатывать общий метод для возврата произвольных типов, потому что вы можете (почти) никогда не гарантировать, что выводимый тип будет доставлен. Исключением являются такие методы, как Collections.emptyList()
, в случае которых пустота списка (и стирание родового типа) является причиной того, что любой вывод для <T>
будет работать:
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}