Подтвердить что ты не робот

Переменная вывода имеет несовместимые границы. Регрессия компилятора Java 8?

Следующая программа компилируется в Java 7 и в Eclipse Mars RC2 для Java 8:

import java.util.List;

public class Test {

    static final void a(Class<? extends List<?>> type) {
        b(newList(type));
    }

    static final <T> List<T> b(List<T> list) {
        return list;
    }

    static final <L extends List<?>> L newList(Class<L> type) {
        try {
            return type.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Используя компилятор javac 1.8.0_45, сообщается следующая ошибка компиляции:

Test.java:6: error: method b in class Test cannot be applied to given types;
        b(newList(type));
        ^
  required: List<T>
  found: CAP#1
  reason: inference variable L has incompatible bounds
    equality constraints: CAP#2
    upper bounds: List<CAP#3>,List<?>
  where T,L are type-variables:
    T extends Object declared in method <T>b(List<T>)
    L extends List<?> declared in method <L>newList(Class<L>)
  where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    CAP#1 extends List<?> from capture of ? extends List<?>
    CAP#2 extends List<?> from capture of ? extends List<?>
    CAP#3 extends Object from capture of ?

Обходной путь заключается в локальном назначении переменной:

import java.util.List;

public class Test {

    static final void a(Class<? extends List<?>> type) {

        // Workaround here
        List<?> variable = newList(type);
        b(variable);
    }

    static final <T> List<T> b(List<T> list) {
        return list;
    }

    static final <L extends List<?>> L newList(Class<L> type) {
        try {
            return type.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Я знаю, что вывод типа сильно изменился в Java 8 (например, из-за JEP 101 "обобщенный вывод целевого типа" ). Итак, это ошибка или новая "особенность" языка?

EDIT. Я также сообщил об этом Oracle как JI-9021550, но на всякий случай это "функция" в Java 8, я также сообщил об ошибке Eclipse:

4b9b3361

Ответ 1

Спасибо за отчет об ошибках, и спасибо, Хольгер, за пример в вашем ответе. Эти и некоторые другие, наконец, заставили меня задать одно небольшое изменение, сделанное в компиляторе Eclipse 11 лет назад. Дело в том, что Eclipse незаконно расширила алгоритм захвата, чтобы применить рекурсивно к границам подстановочных знаков.

Был один пример, когда это незаконное изменение полностью выровняло поведение Eclipse с javac. Поколения разработчиков Eclipse доверяли этому старому решению больше, чем то, что мы могли видеть в JLS. Сегодня я считаю, что предыдущее отклонение должно было иметь другую причину.

Сегодня я взял на себя смелость выровнять ecj с JLS в этом отношении, и вуаля 5 ошибок, которые, как оказалось, чрезвычайно трудно взломать, по существу были решены именно так (плюс небольшая настройка здесь и там в качестве компенсации).

Ergo: Да, у Eclipse была ошибка, но эта ошибка была исправлена ​​с 4.7 вехой 2:)

Здесь какой ecj будет отчитываться отныне:

The method b(List<T>) in the type Test is not applicable for the arguments (capture#1-of ? extends List<?>)

Это подстановочный знак внутри привязки, который не находит правила для обнаружения совместимости. Точнее, некоторое время во время вывода (точнее, включение) мы сталкиваемся со следующим ограничением (T # 0, представляющим переменную вывода):

⟨T#0 = ?⟩

Наивно, мы могли бы просто разрешить переменную типа для подстановочного знака, но - предположительно, потому что подстановочные знаки не считаются типами - правила сокращения определяют выше, как сокращение до FALSE, что позволяет сделать вывод недействительным.

Ответ 2

Отказ от ответственности - я не знаю достаточно о предмете, и следующее - неофициальное рассуждение, чтобы попытаться оправдать поведение javac.


Мы можем свести задачу к

<X extends List<?>> void a(Class<X> type) throws Exception
{
    X instance = type.newInstance();
    b(instance);  // error
}

<T> List<T> b(List<T> list) { ... }

Чтобы сделать вывод T, мы имеем ограничения

      X <: List<?>
      X <: List<T>

По существу, это неразрешимо. Например, no T существует, если X=List<?>.

Не уверен, как Java7 это может случиться. Но javac8 (и IntelliJ) ведет себя "разумно", я бы сказал.


Теперь, как это работает?

    List<?> instance = type.newInstance();
    b(instance);  // ok!

Он работает благодаря подстановочному захвату, который вводит больше информации о типе, "сужая" тип instance

    instance is List<?>  =>  exist W, where instance is List<W>  =>  T=W

К сожалению, это не делается, когда instance есть X, таким образом, существует меньше информации о типе, с которой можно работать.

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

    instance is X, X is List<?>  =>  exist W, where instance is List<W>

Ответ 3

Благодаря ответу bayou.ios мы можем сузить проблему до того, что

<X extends List<?>> void a(X instance) {
    b(instance);  // error
}
static final <T> List<T> b(List<T> list) {
    return list;
}

вызывает ошибку, когда

<X extends List<?>> void a(X instance) {
    List<?> instance2=instance;
    b(instance2);
}
static final <T> List<T> b(List<T> list) {
    return list;
}

может быть скомпилирован без проблем. Назначение instance2=instance - это расширяющееся преобразование, которое также должно происходить для аргументов вызова метода. Таким образом, разница в шаблоне этого ответа является дополнительным отношением подтипа.


Обратите внимание, что пока я не уверен, соответствует ли этот конкретный случай спецификации языка Java, некоторые тесты показали, что Eclipse, принимающий код, вероятно, связано с тем, что он более неряшлив в отношении общих типов в целом, поскольку следующее, определенно некорректный, код может быть скомпилирован без какой-либо ошибки или предупреждения:

public static void main(String... arg) {
    List<Integer> l1=Arrays.asList(0, 1, 2);
    List<String>  l2=Arrays.asList("0", "1", "2");
    a(Arrays.asList(l1, l2));
}
static final void a(List<? extends List<?>> type) {
    test(type);
}
static final <Y,L extends List<Y>> void test(List<L> type) {
    L l1=type.get(0), l2=type.get(1);
    l2.set(0, l1.get(0));
}