Опускание <?> Unintuitly нарушает этот код

Я создал MWE, где изменение одной строки путем добавления <?> решает ошибку компилятора.

Следующий код не компилируется:

import java.util.List;

public class MainClass {

    public void traverse() {
        List<MyEntity> list = null /* ... */;
        for (MyEntity myEntity : list) {
            for (String label : myEntity.getLabels()) { // <-- Offending Line
                /* ... */
            }
        }
    }

    interface MyEntity<T> {
        T get();

        List<String> getLabels();
    }
}

Ошибка компилятора:

Error:(9, 51) java: incompatible types: java.lang.Object cannot be converted to java.lang.String

Изменение определения в строке нарушения от MyEntity myEntity до MyEntity<?> myEntity решает проблему. Интересно, почему тип возврата для каждого из них рассматривается как Object, а не как String, если я не добавлю шаблон к родительскому классу? Обратите внимание, что getLabels() сам по себе не содержит дженериков.

Согласно Глава 14.14.2. спецификации языка Java, каждый из них скомпилирован в цикл с использованием итератора. Интересно, что расширение каждого из них для такого итератора вручную работает:

Iterator<String> iterator = myEntity.getLabels().iterator();
while (iterator.hasNext()) {
    String label = iterator.next();
    /* ... */
}

Может кто-нибудь объяснить, почему?

Ответ 1

Прежде всего, тело метода вашего примера кода можно упростить, чтобы:

public void traverse() {
    MyEntity myEntity = null;
    for (String label : myEntity.getLabels()) { // <--  Offending Line
            /* ... */
    }
}

Почему это происходит? Потому что, когда вы объявляете переменную myEntity (она не имеет значения, где-in for-loop или как в моем примере) в качестве MyEntity myEntity, вы объявляете ее как raw, что исключает общий тип из return type метода getLabels: так что он становится таким же, как List getLabels();, где, очевидно, тип Object ожидается для построения цикла.

В то же время Iterator<String> iterator = myEntity.getLabels().iterator(); работает отлично, потому что вы явно указываете тип: Iterator<String>.


Очень похожий пример приведен в JLS 4.8 "Необработанные типы" , который объясняет, почему это происходит:

... Унаследованные члены типа, зависящие от переменных типа, будут унаследованные как необработанные типы, как следствие того, что правило супертипы сырого типа стираются...

Другое значение вышеприведенных правил состоит в том, что общий внутренний класс исходного типа может использоваться только как необработанный тип:

class Outer<T>{
     class Inner<S> {
         S s;
     }
}

Невозможно получить доступ к Inner как к частично сырым типам ( "редкий" тип):

Outer.Inner<Double> x = null; // illegal

UPD-2. Когда я получил вопросы о Iterator<String> iterator = myEntity.getLabels().iterator();, почему это нормально, в то время как первый пример не работает?

Я лично согласен, что это выглядит запутанным. Но таковы правила. Этот случай также рассматривается в том же параграфе JLS в этом примере:

class Cell<E> {
    E value;

    Cell(E v)     { value = v; }
    E get()       { return value; }
    void set(E v) { value = v; }

    public static void main(String[] args) {
        Cell x = new Cell<String>("abc");
        System.out.println(x.value);  // OK, has type Object
        System.out.println(x.get());  // OK, has type Object
        x.set("def");                 // unchecked warning
    }
}

Более подробное объяснение того, почему Iterator<String> iterator = myEntity.getLabels().iterator(); работает из JLS, основывается на этом правиле:

То есть, правила подтипирования (§4.10.2) программирования Java язык позволяют назначать переменную типа raw значение любого из параметрируемых экземпляров типа

Таким же образом вы всегда можете написать хорошо скомпилированный код как:

    List<String> labels = myEntity.getLabels();
    for (String label : labels) { // <-- OK, no error here
            /* ... */
    }