Warum lässt sich dieses Java 8-Programm nicht kompilieren?

Lesezeit: 7 Minuten

Benutzer-Avatar
ghik

Dieses Programm lässt sich problemlos in Java 7 (oder in Java 8 mit -source 7), kann aber 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 ein rückwärts Quellinkompatibilität zwischen Java 7 und 8. Ich habe es durchgemacht Inkompatibilitäten zwischen Java SE 8 und Java SE 7 Liste, aber nichts gefunden, was zu meinem Problem passen würde.

Also, ist das ein Fehler?

Umfeld:

$ /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)

  • Siehe auch: stackoverflow.com/questions/22509488/…

    – Michael Berry

    14. April 2014 um 15:08 Uhr

  • Acceptor<?> acceptor = new Acceptor<Impl>(new Impl()) sollte gut funktionieren bzw Acceptor<Impl> acceptor = new Acceptor<>(new Impl()).

    – Edwin Dalorzo

    14. April 2014 um 15:24 Uhr


  • Klingt für mich nach einem Bug. Sieht so aus, als wäre der Diamantoperator verwirrt.

    – Bhesh Gurung

    14. April 2014 um 16:05 Uhr


  • das kompiliert gut für mich. mit Downloads von oracle.com/technetwork/articles/java/lambda-1984522.html im Januar

    – Ray Tayek

    14. April 2014 um 16:40 Uhr

  • @AleksandrDubinsky Warum?

    – ghik

    16. April 2014 um 18:20 Uhr

Die Java Language Specification hat sich bzgl Typ Inferenz. In JLS7, Typ Inferenz wurde darin beschrieben §15.12.2.7 und §15.12.2.8wohingegen in JLS8 ein ganzes Kapitel gewidmet ist Kapitel 18. Typinferenz.

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

Diese Inferenzstrategie unterscheidet sich von der Java SE 7 Edition der Java Language Specification [..].

Die Absicht der Änderung war jedoch, rückwärtskompatibel zu sein. Siehe den letzten Absatz des Abschnitts §18.5.2:

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

Ob das stimmt kann ich nicht sagen. Es gibt jedoch einige interessante Variationen Ihres Codes, die das Problem nicht zeigen. Beispielsweise wird die folgende Anweisung ohne Fehler kompiliert:

new Acceptor<>(new Impl());

In diesem Fall gibt es keine Zieltyp. Das bedeutet die Klasseninstanz-Erstellungsausdruck ist kein Poly-Ausdrückeund die Regeln für Typ Inferenz sind einfacher. Sehen §18.5.2:

Wenn der Aufruf kein Poly-Ausdruck ist, sei die gebundene Menge B3 dasselbe sein wie B2.

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

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

Obwohl es im Kontext des Ausdrucks einen Typ gibt, zählt er nicht als Zieltyp. Wenn ein Klasseninstanzerstellungsausdruck passiert in beiden nicht Zuweisungsausdruck oder ein Aufrufausdruckdann kann es nicht sein Poly-Ausdruck. Sehen §15.9:

Ein Klasseninstanzerstellungsausdruck ist ein Polyausdruck (§15.2), wenn er die Rautenform für Typargumente für die Klasse verwendet und in einem Zuweisungskontext oder einem Aufrufkontext erscheint (§5.2, §5.3). Andernfalls handelt es sich um einen eigenständigen Ausdruck.

Um auf Ihre Aussage zurückzukommen. Der relevante Teil ist wieder der JLS8 §18.5.2. Ich kann Ihnen jedoch nicht sagen, ob die folgende Aussage laut JLS8 richtig ist, oder ob der Compiler mit der Fehlermeldung richtig liegt. Aber zumindest haben Sie einige Alternativen und Hinweise für weitere Informationen erhalten.

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

  • Vielen Dank für die ausführliche Erklärung. Ich wusste von den Problemumgehungen, entschied aber, dass ich gut verstehen muss, warum ich Kompilierungsfehler in unserer großen Codebasis erhalte, bevor ich sie auf Java 8 migriere.

    – ghik

    14. April 2014 um 22:24 Uhr

  • Ich würde davon ausgehen, dass die letzte Anweisung einen Fehler verursachen würde, da new Acceptor<?>(...) ist in beiden Versionen nicht erlaubt. Wenn überhaupt, ist es ein Fehler in der Typinferenz von Java7

    – aepurniet

    16. April 2014 um 15:17 Uhr

  • @aepurniet Java 7 folgert es als new Acceptor<Impl>(...). AFAIK, die Typinferenz von Java 7 berücksichtigt niemals den erwarteten Ergebnistyp.

    – ghik

    16. April 2014 um 19:36 Uhr

  • List<Integer> list = new ArrayList<>(); es müsste, da die vorherige Aussage nicht genügend Informationen enthält, um eine Schlussfolgerung zu ziehen.

    – aepurniet

    16. April 2014 um 20:58 Uhr

  • @aepurniet Ach ja, egal. Die Diamond-Inferenz unterscheidet sich von der Methodentyp-Parameter-Inferenz.

    – ghik

    16. April 2014 um 22:22 Uhr

Danke für den Bericht. Das sieht nach einem Fehler aus. 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 abgelegt JDK-8043926um es zu verfolgen.

Benutzer-Avatar
Alexander Dubinsky

Der Typrückschluss wurde in Java 8 geändert. Jetzt betrachtet der Typrückschluss sowohl den Zieltyp als auch die Parametertypen, sowohl für Konstruktoren als auch für 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 ok (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 ergibt natürlich einen Fehler in beiden:

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

Dies ist auch in Java 8 ok:

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 einen Fehler in beiden, aber der Fehler ist anders:

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

Offensichtlich hat Java 8 das Typinferenzsystem intelligenter gemacht. Führt dies zu Inkompatibilitäten? Generell nein. Aufgrund der Typlöschung spielt es eigentlich 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 handhabt. Anstatt sie als fehlende Einschränkung zu betrachten, scheint es sie als einschränkende Einschränkung zu behandeln, die es nicht erfüllen kann. Ich bin mir nicht sicher, ob es dem Buchstaben der JLS folgt, aber ich würde dies zumindest im Geiste als Fehler bezeichnen.

Zu Ihrer Information, das funktioniert (beachten Sie, dass meine Acceptor hat nicht die Typbeschränkungen wie bei Ihnen):

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

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

  • -1, new Foo<>(..) berücksichtigt definitiv die Konstruktorargumente für die Inferenz.

    – Tavian Barnes

    18. April 2014 um 20:26 Uhr

  • @TavianBarnes möchten Sie ein Zitat angeben?

    – Aleksandr Dubinsky

    18. April 2014 um 21:23 Uhr


  • @TavianBarnes du hast recht. Sieht so aus, als ob in Java 8 die Typinferenz sowohl Parameter als auch den Zieltyp sowohl für Konstruktoren als auch für Methoden betrachtet. Ein bisschen unfair von Ihnen gegenüber -1, weil Sie sich nur in einem Teil meiner Antwort geirrt haben und der ich AFAIK vorangestellt habe.

    – Aleksandr Dubinsky

    18. April 2014 um 23:53 Uhr

  • Fair genug, ich werde die -1 entfernen, sobald Sie die Antwort bearbeiten. Java 7 verwendete übrigens auch Konstruktorparameter für die Inferenz.

    – Tavian Barnes

    19. April 2014 um 0:04 Uhr

  • @TavianBarnes Ich habe meine Antwort bearbeitet. Ich habe viel gelernt, danke.

    – Aleksandr Dubinsky

    19. April 2014 um 0:15 Uhr

1200410cookie-checkWarum lässt sich dieses Java 8-Programm nicht kompilieren?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy