Почему компиляция этого кода вызывает переполнение стека компилятора?

interface Pong<T> {}
class Ping<T> implements Pong<Pong<? super Ping<Ping<T>>>> {
    static void Ping() {
        Pong<? super Ping<Long>> Ping = new Ping<Long>();
    }
}

Попытка скомпилировать это дает ошибку:

The system is out of resources.
Consult the following stack trace for details.
java.lang.StackOverflowError
    at com.sun.tools.javac.code.Types$23.visitClassType(Types.java:2579)
    at com.sun.tools.javac.code.Type$ClassType.accept(Type.java:554)
    at com.sun.tools.javac.code.Types$UnaryVisitor.visit(Types.java:3260)
    at com.sun.tools.javac.code.Types$23.visitClassType(Types.java:2592)
    at com.sun.tools.javac.code.Types$23.visitClassType(Types.java:2579)
    at com.sun.tools.javac.code.Type$ClassType.accept(Type.java:554)
    ...

Код предоставлен etorreborre на github.

Ответ 1

Очевидно, что это ошибка в компиляторе Java. Компилятор не должен терпеть крах, особенно в такой маленькой программе.

Это может быть даже ямкой в ​​Спецификации языка Java; то есть неясным краевым случаем в дженериках, которые авторы JLS не рассматривали.

Но (IMO) это не что иное, как любопытство, если вы не придумаете пример, который не так явно умудрился разбить компилятор. Я имею в виду, что этот примерный код не имеет особого значения...


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

Ответ 2

Поскольку компилятор не может решить, является ли Long Pong, который является super для Ping для Ping для Long, или является Ping для Ping того, что расширяет a Pong a Pong... но я могу ошибаться.

Ответ 3

У меня есть коллега, которая имеет аналогичную проблему в реальном коде. Там он имеет абстрактный базовый класс с 2 параметрами типа, который имеет два подкласса, фиксирующих их для конкретных типов. В основном это позволяет определить полную логику в абстрактном классе вместо того, чтобы дублировать код в обоих подклассах с замененными конкретными типами.

В принципе, код есть что-то в этом роде:

public abstract class AImpl<X extends A<Y>, Y extends A<X>> {
    public X foo(Y o) {
        return o.doStuff();
    }

    public Y bar(X o) {
        return o.doStuff();
    }
}

class VImpl extends AImpl<V, E> {}
class EImpl extends AImpl<E, V> {}

interface A<T> {
    T doStuff();
}

interface V extends A<E> {}
interface E extends A<V> {}

Этот код действительно компилируется. На самом деле существует не только два подкласса, но и более глубокая иерархия типов, например три варианта VImpl и EImpl, каждая из которых имеет произвольное множество подклассов. Ну и на самом деле есть 3 типа параметров, и ограничения немного сложнее, как показано выше.

Когда было только два варианта VImpl и EImpl, он все еще был скомпонован. Как только были добавлены третьи варианты, он получил переполнение стека в компиляторе. Тем не менее, компилятор Eclipse все еще способен скомпилировать код, поэтому кажется, что он просто javac делает что-то рекурсивно, что лучше делать итеративно.

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

Ответ 4

Я испытал то же самое с некоторыми родовыми вещами в JDK 8_u25, обновление до JDK 8u_65 разрешило проблему.