Нижняя ограниченная дикая карта вызывает проблемы в javac, но не Eclipse

Этот фрагмент кода компилируется в Eclipse, но не в javac:

import java.util.function.Consumer;

public class Test {
    public static final void m1(Consumer<?> c) {
        m2(c);
    }
    private static final <T> void m2(Consumer<? super T> c) {
    }
}

Выход javac:

C:\Users\lukas\workspace>javac -version
javac 1.8.0_92

C:\Users\lukas\workspace>javac Test.java
Test.java:5: error: method m2 in class Test cannot be applied to given types;
        m2(c);
        ^
  required: Consumer<? super T>
  found: Consumer<CAP#1>
  reason: cannot infer type-variable(s) T
    (argument mismatch; Consumer<CAP#1> cannot be converted to Consumer<? super T>)
  where T is a type-variable:
    T extends Object declared in method <T>m2(Consumer<? super T>)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error
----------------------------------------------------------------

Какой компилятор ошибочен и почему? (Отчет об ошибках Eclipse и части обсуждения здесь)

Ответы

Ответ 1

Этот код является законным по JLS 8. javac версии 8 и ранее имели несколько ошибок в том, как они обрабатывают подстановочные знаки и захваты. Начиная с версии 9 (ранний доступ, я пробовал версию ea-113 и новее), также javac принимает этот код.

Чтобы понять, как компилятор анализирует это в соответствии с JLS, важно рассказать обо всем, что такое подстановочные знаки, переменные типа, переменные вывода и т.д.

Тип c равен Consumer<capture#1-of ?> (javac будет писать Consumer<CAP#1>). Этот тип неизвестен, но исправлен.

Параметр m2 имеет тип Consumer<? super T>, где T - это переменная типа, которая создается экземпляром типа.

Во время ввода типа для представления T используется переменная вывода, обозначаемая ecj как T#0.

Вывод типа состоит в определении того, может ли T#0 быть экземпляр любым типом без нарушения каких-либо ограничений типа. В этом конкретном случае мы начнем с этого противопоказания:

⟨c → Consumer <? super T # 0 > ⟩

который ступенчато уменьшается (применяя JLS 18.2):

⟨Конкурс < захват # 1-of? > → Потребитель <? super T # 0 > ⟩

⟨capture # 1-of? <= & alpha; super T # 0⟩

⟨T # 0 <: capture # 1-of?⟩

T # 0 <: capture # 1-of?

Последняя строка - это "привязка типа" и выполняется сокращение. Поскольку никаких дополнительных ограничений не требуется, разрешение тривиально создает T#0 для типа capture#1-of ?.

К этим шагам вывод типа доказал, что m2 применим для этого конкретного вызова. QED.

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

Ответ 2

Обратите внимание, что следующее может быть скомпилировано без проблем:

public class Test {
    public static final void m1(Consumer<?> c) {
        m2(c);
    }
    private static final <T> void m2(Consumer<T> c) {
    }
}

Несмотря на то, что мы не знаем фактического типа потребителя, мы знаем, что он будет присваиваться Consumer<T>, хотя мы не знаем, что T (не знаю, что такое T, является нормой в общем коде в любом случае).

Но если присвоение Consumer<T> допустимо, значит, присвоение Consumer<? super T> будет. Мы можем даже показать это практически с промежуточным шагом:

public class Test {
    public static final void m1(Consumer<?> c) {
        m2(c);
    }
    private static final <T> void m2(Consumer<T> c) {
        m3(c);
    }
    private static final <T> void m3(Consumer<? super T> c) {
    }
}

Нет объектов компилятора.

Он также будет принят при замене дикой карты на именованный тип, например

public class Test {
    public static final void m1(Consumer<?> c) {
        m2(c);
    }
    private static final <E,T extends E> void m2(Consumer<E> c) {
    }
}

Здесь E является супер-типом T, как и ? super T.

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

Единственное, что имеет значение, состоит в том, что он уже исправлен в Java 9.