Warum erlaubt Javac einige unmögliche Besetzungen und andere nicht?

52

Wenn ich versuche, a Stringin a java.util.Dateumzuwandeln, fängt der Java-Compiler den Fehler ab. Warum kennzeichnet der Compiler Folgendes nicht als Fehler?

List<String> strList = new ArrayList<>();                                                                      
Date d = (Date) strList;

Natürlich wirft die JVM a ClassCastException löst zur Laufzeit eine aus, aber der Compiler kennzeichnet sie nicht.

Das Verhalten ist das gleiche mit javac 1.8.0_212 und 11.0.2.

Mike Woinoski
quelle
2
Hier ist nichts Besonderes List. Date d = (Date) new Object();
Elliott Frisch
1
Ich habe in letzter Zeit mit einem Arduino gespielt. Ich würde einen Compiler lieben, der keine Besetzung gerne akzeptiert und sie dann einfach mit völlig unvorhersehbaren Ergebnissen erstellt hat. String zu Integer? Sichere Sache! Double to Integer? Jawohl! String zu boolesch? Zumindest wird das meistens falsch ...
Stian Yttervik
@ElliottFrisch: Es gibt eine offensichtliche Vererbungsbeziehung zwischen Datum und Objekt, aber keine Beziehung zwischen Datum und Liste. Daher habe ich erwartet, dass der Compiler diese Besetzung markiert, genauso wie er eine Besetzung von String bis Datum kennzeichnet. Aber wie Zabuza in ihrer hervorragenden Antwort erklärt, ist List eine Schnittstelle, daher wäre die Besetzung legal, wenn strListes sich um eine Instanz einer Klasse handelt, die List implementiert.
Mike Woinoski
Dies ist eine häufig wiederkehrende Frage, und ich bin sicher, dass ich mehrere Duplikate davon gesehen habe. Es ist im Grunde die umgekehrte Version des stark verwandten: stackoverflow.com/questions/21812289/…
Hulk
1
@StianYttervik -fpermissive ist was das macht. Aktivieren Sie die Compiler-Warnungen.
Bobburner

Antworten:

86

Die Besetzung ist technisch möglich. Javac kann nicht einfach beweisen, dass dies in Ihrem Fall nicht der Fall ist, und das JLS definiert dies tatsächlich als gültiges Java-Programm. Daher wäre es falsch, einen Fehler zu kennzeichnen.

Dies liegt daran, dass Listes sich um eine Schnittstelle handelt. Sie könnten also eine Unterklasse von a haben Date, die tatsächlich Listwie Listhier getarnt implementiert - und dann Datewäre es vollkommen in Ordnung, sie zu besetzen. Zum Beispiel:

public class SneakyListDate extends Date implements List<Foo> {
    ...
}

Und dann:

List<Foo> list = new SneakyListDate();
Date date = (Date) list; // This one is valid, compiles and runs just fine

Das Erkennen eines solchen Szenarios ist möglicherweise nicht immer möglich, da Laufzeitinformationen erforderlich sind, wenn die Instanz beispielsweise von einer Methode stammt. Und selbst wenn, würde es viel mehr Aufwand für den Compiler erfordern. Der Compiler verhindert nur Casts, die absolut unmöglich sind, da der Klassenbaum überhaupt nicht übereinstimmen kann. Was hier nicht der Fall ist, wie man sieht.

Beachten Sie, dass das JLS erfordert, dass Ihr Code ein gültiges Java-Programm ist. In 5.1.6.1. Zulässige schmale Referenzkonvertierung heißt es:

Eine verengende Referenzkonvertierung von Referenztyp Szu Referenztyp ist vorhanden, Twenn alle der folgenden Bedingungen erfüllt sind :

  • [...]
  • Einer der folgenden Fälle gilt :
    • [...]
    • Sist ein Schnittstellentyp, Tist ein Klassentyp und Tbenennt keine finalKlasse.

Also auch wenn der Compiler könnte herausfinden , dass Ihr Fall tatsächlich nachweislich unmöglich ist, darf er keinen Fehler melden, da das JLS ihn als gültiges Java-Programm definiert.

Es darf nur eine Warnung angezeigt werden.

Zabuzard
quelle
16
Es ist erwähnenswert, dass der Grund für den Fall mit String darin besteht, dass String endgültig ist, sodass der Compiler weiß, dass keine Klasse ihn erweitern kann.
MTilsted
5
Eigentlich glaube ich nicht, dass es die "Endgültigkeit" von String ist, die zum myDate = (Date) myStringScheitern führt. Unter Verwendung der JLS-Terminologie versucht die Anweisung, von S(the String) nach T(the Date) zu konvertieren . Da Ses sich nicht um einen Schnittstellentyp handelt, gilt die oben angegebene JLS-Bedingung nicht. Wenn Sie beispielsweise versuchen, einen Kalender in ein Datum umzuwandeln, wird ein Compilerfehler angezeigt, obwohl keine der beiden Klassen endgültig ist.
Mike Woinoski
1
Ich weiß nicht, ob der Compiler enttäuscht werden kann oder nicht, um zu beweisen, dass strList immer nur vom Typ ArrayList sein kann.
Joshua
3
Dem Compiler ist die Überprüfung nicht untersagt. Es ist jedoch verboten, es als Fehler zu bezeichnen. Das würde den Compiler nicht konform machen. (Siehe meine Antwort ...)
Stephen C
3
Um ein wenig Kauderwelsch hinzufügen, würde der Compiler beweisen müssen , dass der Typ Date & Listist unbewohnbar , ist es nicht genug , um zu beweisen , dass es unbewohnt zur Zeit (es auch in Zukunft sein könnte).
Polygnome
15

Betrachten wir eine Verallgemeinerung Ihres Beispiels:

List<String> strList = someMethod();       
Date d = (Date) strList;

Dies sind die Hauptgründe, warum Date d = (Date) strList;kein Kompilierungsfehler vorliegt.

  • Der intuitive Grund ist, dass der Compiler (im Allgemeinen) den genauen Typ des von diesem Methodenaufruf zurückgegebenen Objekts nicht kennt. Es ist möglich, dass es nicht nur eine Klasse ist, die implementiert List, sondern auch eine Unterklasse von Date.

  • Der technische Grund ist, dass die Java-Sprachspezifikation die Verengung der Referenzkonvertierung "erlaubt", die dieser Typumwandlung entspricht. Gemäß JLS 5.1.6.1 :

    "Eine verengende Referenzkonvertierung ist von Referenztyp Szu Referenztyp vorhanden, Twenn alle der folgenden Bedingungen erfüllt sind:"

    ...

    5) " Sist ein Schnittstellentyp, Tist ein Klassentyp und Tbenennt keine finalKlasse."

    ...

    An einer anderen Stelle sagt JLS auch, dass zur Laufzeit eine Ausnahme ausgelöst werden kann ...

    Beachten Sie, dass die Bestimmung von JLS 5.1.6.1 ausschließlich auf den deklarierten Typen der beteiligten Variablen und nicht auf den tatsächlichen Laufzeittypen basiert . Im allgemeinen Fall kennt und kann der Compiler die tatsächlichen Laufzeitarten nicht kennen.


Warum kann der Java-Compiler nicht herausfinden, dass die Besetzung nicht funktioniert?

  • In meinem Beispiel someMethodkönnte der Aufruf Objekte mit verschiedenen Typen zurückgeben. Selbst wenn der Compiler in der Lage war, den Methodenkörper zu analysieren und den genauen Satz von Typen zu bestimmen, die zurückgegeben werden könnten, kann nichts jemanden daran hindern, ihn zu ändern, um verschiedene Typen zurückzugeben ... nachdem er den Code kompiliert hat, der ihn aufruft. Dies ist der Hauptgrund, warum JLS 5.1.6.1 sagt, was es sagt.

  • In Ihrem Beispiel ein intelligenter Compiler könnte herausfinden , dass die Besetzung nie gelingen kann. Außerdem darf eine Warnung zur Kompilierungszeit ausgegeben werden , um auf das Problem hinzuweisen.

Warum darf ein Smart Compiler nicht sagen, dass dies überhaupt ein Fehler ist?

  • Weil das JLS sagt, dass dies ein gültiges Programm ist. Zeitraum. Jeder Compiler, der dies als Fehler bezeichnet , ist nicht Java-kompatibel.

  • Außerdem ist jeder Compiler, der Java-Programme ablehnt, von denen JLS und andere Compiler sagen, dass sie gültig sind, ein Hindernis für die Portabilität von Java-Quellcode.

Stephen C.
quelle
4
Stimmen Sie dafür zu, dass sich die aufgerufene Funktionsimplementierung nach dem Kompilieren der aufrufenden Klasse möglicherweise ändert . Selbst wenn zum Zeitpunkt der Kompilierung mit der aktuellen Implementierung des Angerufenen nachweisbar ist, dass die Besetzung unmöglich ist, ist dies zu späteren Laufzeiten möglicherweise nicht der Fall wenn der Angerufene gewechselt oder ersetzt wurde.
Peter - Stellen Sie Monica
2
Upvote zur Hervorhebung des Portabilitätsproblems, das auftreten würde, wenn ein Compiler versucht, zu intelligent zu sein.
Mike Woinoski
2

5.5.1. Referenztyp Casting:

Bei einem Referenztyp zur Kompilierungszeit S(Quelle) und einem Referenztyp zur Kompilierungszeit T(Ziel) besteht eine Casting-Konvertierung von Szu, Twenn aufgrund der folgenden Regeln keine Fehler zur Kompilierungszeit auftreten.

[...]

Wenn Sist ein Schnittstellentyp:

  • [...]

  • Wenn Tes sich um einen Klassen- oder Schnittstellentyp handelt, der nicht endgültig ist, dann gibt es einen Supertyp Xvon Tund einen Supertyp Yvon S, so dass beide Xund Ynachweislich unterschiedliche parametrisierte Typen sind und dass die Löschungen von Xund gleich Ysind, ein Fehler bei der Kompilierung tritt ein.

    Andernfalls ist die Besetzung zum Zeitpunkt der Kompilierung immer legal (denn selbst wenn Tsie nicht implementiert wird S, handelt es sich um eine Unterklasse von TMacht).

List<String>ist Sund Dateist Tin deinem Fall.

Oleksandr Pyrohov
quelle