Верхня межа загального типу повернення - інтерфейс проти класу - напрочуд вірний код


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
Federico Peralta Schaffner

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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.