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

171

Это пример из стороннего библиотечного 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();нет?

Адам Михалик
источник
15
интересный! кастинг на LHS Integer x = getCharSequence();будет компилироваться, но кастинг на RHS Integer x = (Integer) getCharSequence();не скомпилируется
хлопья
Какую версию Java-компилятора вы используете? Пожалуйста, укажите эту информацию в вопросе.
Федерико Перальта Шаффнер
@FedericoPeraltaSchaffner не может понять, почему это важно - это вопрос непосредственно о JLS.
Борис Паук
@BoristheSpider Потому что механизм определения типа изменился для java8
Федерико Перальта Шаффнер
1
@FedericoPeraltaSchaffner - я пометил вопрос уже с [java-8], но я добавил версию компилятора в пост сейчас.
Адам Михалик

Ответы:

184

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>

но только первая граница может быть неинтерфейсом.

Пол Боддингтон
источник
62
Я понятия не имел, что вы можете вставить &в общее определение. +1
хлопья
13
@flkes Вы можете указать более одного, но только первый аргумент может быть неинтерфейсом. <T extends String & List & Comparator>Это нормально, но <T extends String & Integer>это не так, потому что Integerэто не интерфейс.
Пол Боддингтон
7
@PaulBoddington Существует некоторое практическое использование этих методов. Например, если тип фактически не используется для хранимых данных. Примеры для этого, Collections.emptyList()а также Optional.empty(). Они возвращают реализации универсального интерфейса, но ничего не хранят.
Стефан Доллаз
6
И никто не говорит, что класс, находящийся finalво время компиляции, будет finalво время выполнения.
Хольгер
7
@Federico Peralta Schaffner: суть в том, что метод getCharSequence()обещает вернуть все, Xчто нужно вызывающему, включая возврат расширяющего Integerи реализующего типа, CharSequenceесли это необходимо, и в соответствии с этим обещанием правильно разрешить присваивать результат Integer. Это метод, getCharSequence()который не работает, поскольку он не выполняет свое обещание, но это не ошибка компилятора.
Хольгер
59

Тип, который выводится вашим компилятором до назначения, 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;
}
Лукас Эдер
источник