Общая верхняя граница возвращаемого типа - интерфейс против класса - удивительно действительный код

Это реальный пример из 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;
}