Warum wird dieses Java 8-Programm nicht kompiliert?

77

Dieses Programm wird in Java 7 (oder in Java 8 mit -source 7) problemlos kompiliert, kann jedoch nicht mit Java 8 kompiliert werden:

interface Iface<T> {}
class Impl implements Iface<Impl> {}

class Acceptor<T extends Iface<T>> {
    public Acceptor(T obj) {}
}

public class Main {
    public static void main(String[] args) {
        Acceptor<?> acceptor = new Acceptor<>(new Impl());
    }
}

Ergebnis:

Main.java:10: error: incompatible types: cannot infer type arguments for Acceptor<>
        Acceptor<?> acceptor = new Acceptor<>(new Impl());
                                           ^
    reason: inference variable T has incompatible bounds
      equality constraints: Impl
      upper bounds: Iface<CAP#1>,Iface<T>
  where T is a type-variable:
    T extends Iface<T> declared in class Acceptor
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Iface<CAP#1> from capture of ?
1 error

Mit anderen Worten, dies ist eine Inkompatibilität der Rückwärtsquelle zwischen Java 7 und 8. Ich habe die Liste der Inkompatibilitäten zwischen Java SE 8 und Java SE 7 durchgearbeitet, aber nichts gefunden, was zu meinem Problem passen würde.

Also, ist das ein Fehler?

Umgebung:

$ /usr/lib/jvm/java-8-oracle/bin/java -version
java version "1.8.0"
Java(TM) SE Runtime Environment (build 1.8.0-b132)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b70, mixed mode)
Ghik
quelle
2
Siehe auch
Michael Berry
4
Acceptor<?> acceptor = new Acceptor<Impl>(new Impl())sollte gut funktionieren oder Acceptor<Impl> acceptor = new Acceptor<>(new Impl()).
Edwin Dalorzo
Klingt für mich nach einem Fehler. Der Diamantoperator scheint verwirrt zu sein.
Bhesh Gurung
das kompiliert gut für mich. Verwenden von Downloads von oracle.com/technetwork/articles/java/lambda-1984522.html im Januar
Ray Tayek
2
@AleksandrDubinsky Warum?
Ghik

Antworten:

20

Danke für den Bericht. Das sieht aus wie ein Fehler. Ich werde mich darum kümmern und wahrscheinlich eine bessere Antwort hinzufügen, sobald wir mehr Informationen darüber haben, warum dies geschieht. Ich habe diesen Fehlereintrag JDK-8043926 abgelegt , um ihn zu verfolgen.

Vicente Romero
quelle
40

Die Java-Sprachspezifikation hat sich hinsichtlich der Typinferenz erheblich geändert . In JLS7 wurde die Typinferenz in §15.12.2.7 und §15.12.2.8 beschrieben , während in JLS8 ein ganzes Kapitel Kapitel 18 gewidmet ist. Typinferenz .

Die Regeln sind sowohl in JLS7 als auch in JLS8 recht komplex. Es ist schwierig, die Unterschiede zu erkennen, aber es gibt offensichtlich Unterschiede, wie aus Abschnitt 18.5.2 hervorgeht :

Diese Inferenzstrategie unterscheidet sich von der Java SE 7 Edition der Java-Sprachspezifikation [..].

Die Absicht der Änderung war jedoch, abwärtskompatibel zu sein. Siehe den letzten Absatz von Abschnitt §18.5.2 :

[..] Die Strategie ermöglicht in typischen Anwendungsfällen vernünftige Ergebnisse und ist abwärtskompatibel mit dem Algorithmus in der Java SE 7 Edition der Java-Sprachspezifikation.

Ich kann nicht sagen, ob das stimmt oder nicht. Es gibt jedoch einige interessante Variationen Ihres Codes, die das Problem nicht zeigen. Die folgende Anweisung wird beispielsweise fehlerfrei kompiliert:

new Acceptor<>(new Impl());

In diesem Fall gibt es keinen Zieltyp . Das bedeutet, dass der Ausdruck zum Erstellen einer Klasseninstanz kein Polyausdruck ist und die Regeln für die Typinferenz einfacher sind. Siehe §18.5.2 :

Wenn der Aufruf kein Polyausdruck ist, sei die gebundene Menge B 3 dieselbe wie B 2 .

Das ist auch der Grund, warum die folgende Aussage funktioniert.

Acceptor<?> acceptor = (Acceptor<?>) new Acceptor<>(new Impl());

Obwohl es im Kontext des Ausdrucks einen Typ gibt, zählt dieser nicht als Zieltyp . Wenn ein Ausdruck zur Erstellung einer Klasseninstanz weder in einem Zuweisungsausdruck noch in einem Aufrufausdruck vorkommt , kann er kein Polyausdruck sein . Siehe §15.9 :

Ein Ausdruck zur Erstellung einer Klasseninstanz ist ein Polyausdruck (§15.2), wenn er die Diamantform für Typargumente für die Klasse verwendet und in einem Zuweisungskontext oder einem Aufrufkontext (§5.2, §5.3) angezeigt wird. Andernfalls handelt es sich um einen eigenständigen Ausdruck.

Zurück zu Ihrer Aussage. Der relevante Teil des JLS8 ist wieder §18.5.2 . Ich kann Ihnen jedoch nicht sagen, ob die folgende Aussage gemäß JLS8 korrekt ist oder ob der Compiler mit der Fehlermeldung Recht hat. Aber zumindest haben Sie einige Alternativen und Hinweise für weitere Informationen.

Acceptor<?> acceptor = new Acceptor<>(new Impl());
nosid
quelle
Vielen Dank für die gründliche Erklärung. Ich wusste um die Problemumgehungen, entschied aber, dass ich gut verstehen muss, warum ich Kompilierungsfehler in unserer großen Codebasis
erhalte,
Ich würde annehmen, dass die letzte Anweisung einen Fehler verursachen würde, da new Acceptor<?>(...)dies in beiden Versionen nicht zulässig ist. Wenn überhaupt, ist es ein Fehler in der Typinferenz von Java7
Aepurniet
@aepurniet Java 7 schließt daraus als new Acceptor<Impl>(...). AFAIK, Java 7-Typinferenz berücksichtigt niemals den erwarteten Ergebnistyp.
Ghik
List<Integer> list = new ArrayList<>();Dies müsste der Fall sein, da die vorherige Anweisung nicht genügend Informationen enthält, um eine Schlussfolgerung zu ziehen.
Aepurniet
@aepurniet Oh richtig, egal. Die Diamantinferenz unterscheidet sich von der Parameterinferenz des Methodentyps.
Ghik
7

Die Typinferenz wurde in Java 8 geändert. Die Typinferenz untersucht nun sowohl den Zieltyp als auch die Parametertypen für Konstruktoren und Methoden. Folgendes berücksichtigen:

interface Iface {}
class Impl implements Iface {}
class Impl2 extends Impl {}

class Acceptor<T> {
    public Acceptor(T obj) {}
}

<T> T foo(T a) { return a; }

Folgendes ist jetzt in Java 8 in Ordnung (aber nicht in Java 7):

Acceptor<Impl> a = new Acceptor<>(new Impl2());

// Java 8 cleverly infers Acceptor<Impl>
// While Java 7 infers Acceptor<Impl2> (causing an error)

Dies führt natürlich in beiden Fällen zu einem Fehler:

Acceptor<Impl> a = new Acceptor<Impl2>(new Impl2());

Dies ist auch in Java 8 in Ordnung:

Acceptor<Impl> a = foo (new Acceptor<>(new Impl2())); 

// Java 8 infers Acceptor<Impl> even in this case
// While Java 7, again, infers Acceptor<Impl2>
//   and gives: incompatible types: Acceptor<Impl2> cannot be converted to Acceptor<Impl>

Das Folgende gibt in beiden Fällen einen Fehler aus, der jedoch unterschiedlich ist:

Acceptor<Impl> a = foo (new Acceptor<Impl2>(new Impl2()));

// Java 7:
// incompatible types: Acceptor<Impl2> cannot be converted to Acceptor<Impl>

// Java 8:
// incompatible types: inferred type does not conform to upper bound(s)
//     inferred: Acceptor<Impl2>
//     upper bound(s): Acceptor<Impl>,java.lang.Object

Java 8 hat das Typinferenzsystem eindeutig intelligenter gemacht. Verursacht dies Inkompatibilitäten? Im Allgemeinen nein. Aufgrund der Typlöschung spielt es keine Rolle, welche Typen abgeleitet wurden, solange das Programm kompiliert wird. Kompiliert Java 8 alle Java 7-Programme? Es sollte, aber Sie haben einen Fall angesprochen, in dem dies nicht der Fall ist.

Was zu passieren scheint, ist, dass Java 8 Wildcards nicht gut verarbeitet. Anstatt sie als Fehlen einer Einschränkung zu betrachten, scheint es, sie als restriktive Einschränkung zu behandeln, die sie nicht erfüllen können. Ich bin mir nicht sicher, ob es dem Brief der JLS folgt, aber ich würde dies zumindest im Geiste einen Fehler nennen.

Zu AcceptorIhrer Information, dies funktioniert (beachten Sie, dass meine nicht die Typbeschränkungen hat, die Ihre haben):

Acceptor<?> a = new Acceptor<>(new Impl2());

Beachten Sie, dass in Ihrem Beispiel ein Platzhaltertyp außerhalb eines Methodenparameters verwendet wird (was nicht ratsam ist). Ich frage mich, ob das gleiche Problem in typischerem Code auftritt, der den Diamantoperator in Methodenaufrufen verwendet. (Wahrscheinlich.)

Aleksandr Dubinsky
quelle
-1, new Foo<>(..)berücksichtigt definitiv die Konstruktorargumente für die Inferenz.
Tavian Barnes
@TavianBarnes Lust auf ein Zitat?
Aleksandr Dubinsky
@TavianBarnes du hast recht. In Java 8 sieht die Typinferenz beide Parameter und den Zieltyp sowohl für Konstruktoren als auch für Methoden aus. Ein bisschen unfair von Ihnen gegenüber -1, weil Sie sich nur in einem Teil meiner Antwort geirrt haben und die ich mit AFAIK eingeleitet habe.
Aleksandr Dubinsky
Fair genug, ich werde die -1 entfernen, sobald Sie die Antwort bearbeiten. Java 7 verwendete übrigens auch Konstruktorparameter für die Inferenz.
Tavian Barnes
@TavianBarnes Ich habe meine Antwort bearbeitet. Ich habe viel gelernt, danke.
Aleksandr Dubinsky