Der Java-Ternäroperator scheint Integer uneinheitlich auf Ints zu setzen

8

Einer meiner Schüler erhält eine Nullzeiger-Ausnahme, wenn er einen ternären Operator verwendet, der manchmal zu null führt. Ich denke, ich verstehe das Problem, aber es scheint, als ob es aus einer inkonsistenten Art Schlussfolgerung resultiert. Oder anders gesagt, ich habe das Gefühl, dass die Semantik hier widersprüchlich ist und der Fehler vermeidbar sein sollte, ohne seinen Ansatz zu ändern.

Diese Frage ist ähnlich, aber anders als Noch eine Frage zu ternär Betreiber . In dieser Frage muss der Null-Integer auf ein int gezwungen werden, da der Rückgabewert der Funktion ein int ist. Dies ist jedoch in meinem Studentencode nicht der Fall.

Dieser Code läuft gut:

%Vor%

Der Wert von x ist null. Keine NPE. In diesem Fall kann der Compiler herausfinden, dass das Ergebnis des ternären Operators Ganzzahl sein muss. Daher wird die 3 (ein Int) in eine Ganzzahl umgewandelt, anstatt null auf int zu werfen.

Dieser Code wird jedoch ausgeführt:

%Vor%

führt zu einer NPE. Der einzige Grund dafür ist, dass der NULL-Wert in einen int-Wert umgewandelt wird, aber das ist nicht wirklich notwendig und scheint mit dem ersten Bit des Codes inkonsistent zu sein. Das heißt, wenn der Compiler für das erste Snipet ableiten kann, dass das Ergebnis des ternären Operators eine Ganzzahl ist, warum kann er das dann nicht im zweiten Fall tun? Das Ergebnis des zweiten ternären Ausdrucks muss eine Ganzzahl sein, und da das Ergebnis das zweite Ergebnis für den ersten ternären Operator ist, sollte das Ergebnis des ersten ternären Operators auch eine Ganzzahl sein.

Ein anderes Snipet funktioniert gut:

%Vor%

Hier scheint der Compiler folgern zu können, dass das Ergebnis der beiden ternären Operatoren ein Integer ist und forciert den Nullwert nicht auf int.

    
David Stigant 16.10.2015, 18:27
quelle

3 Antworten

10

Der Schlüssel dazu ist, dass der bedingte Operator richtig assoziativ ist. Die Regeln zum Bestimmen des Ergebnistyps eines bedingten Ausdrucks sind scheußlich kompliziert aber es läuft darauf hinaus:

  1. Erstes (5 > 8) ? 4 : null wird ausgewertet, der zweite Operand ist ein int , das dritte ist null . Wenn wir es in der Tabelle nachschlagen, ist der Ergebnistyp dieses Ausdrucks Integer . (Mit anderen Worten: Da einer der Operanden null ist, wird dies als referenzbedingter Ausdruck behandelt)
  2. Dann müssen wir (5>7) ? 3 : <previous result> auswerten, was bedeutet, dass wir in der oben verlinkten Tabelle den Ergebnistyp für den zweiten Operanden int und den dritten Operanden Integer suchen müssen: und es ist int . Dies bedeutet, dass <previous result> entkoppelt werden muss und mit einem NPE .
  3. fehlschlägt

Warum funktioniert der erste Fall dann?

Dort haben wir (5>7) ? 3 : null; , wie wir gesehen haben, wenn der 2. Operand int ist und 3.% null , der Ergebnistyp ist Integer . Aber wir weisen es einer Variablen vom Typ Integer zu, so dass kein Unboxing erforderlich ist.

Dies geschieht jedoch nur mit dem null Literal, der folgende Code wird dennoch eine NPE werfen, weil die Operandentypen int und Integer einen numerischen bedingten Ausdruck ergeben:

%Vor%

Um es zusammenzufassen: Es gibt eine Art Logik, aber es ist keine menschliche Logik.

  1. Wenn beide Operanden vom Kompilierungstyp Integer sind, ist das Ergebnis ein Integer .
  2. Wenn ein Operand vom Typ int ist und der andere Integer , ist das Ergebnis ein int .
  3. Wenn ein Operand null type ist (Der einzige gültige Wert ist der Verweis null ), das Ergebnis ist ein Integer .
biziclop 16.10.2015 18:38
quelle
1
%Vor%

führt zu einer NPE, da null in int übergeben wird.

Dasselbe gilt für Ihren Code

%Vor%

sieht so aus:

%Vor%

Wie Sie sehen, wird NULL erneut an int übergeben, was fehlschlägt.

Um das zu lösen, könnte man das tun:

%Vor%

Nun wird nicht versucht, null auf int, sondern auf Integer zu setzen.

    
Pinkie Swirl 16.10.2015 18:40
quelle
0

Vor Java8 wird in fast allen Fällen der Typ eines Ausdrucks von unten nach oben aufgebaut, ganz abhängig von den Typen der Unterausdrücke; es hängt nicht vom Kontext ab. Das ist schön und einfach, und Code ist leicht zu verstehen; Die Überladungsauflösung hängt beispielsweise von den Argumenten ab, die unabhängig vom Methodenaufrufkontext aufgelöst werden.

( Die einzige Ausnahme, die ich kenne, ist jls # 15.12.2.8 )

Wenn ein bedingter Ausdruck in Form von ?int:Integer angegeben wird, muss die Spezifikation einen festen Typ für ihn ohne Berücksichtigung des Kontexts definieren. Der Typ int wurde gewählt, was vermutlich in den meisten Anwendungsfällen besser ist. Natürlich ist es auch die Quelle von NPE aus dem Unboxing.

In Java8 kann kontextbezogene Typinformation in Typinferenz verwendet werden. Dies ist für viele Fälle praktisch; aber es führt auch zu Verwirrung, da es zwei Richtungen geben kann, um den Typ eines Ausdrucks aufzulösen. Zum Glück sind einige Ausdrücke noch eigenständig; ihre Typen sind kontextunabhängig.

w.r.t bedingte Ausdrücke, wir wollen nicht einfache wie false?0:1 kontextabhängig sein; ihre Typen sind selbstverständlich. Auf der anderen Seite wollen wir kontextuelle Typrückschlüsse auf kompliziertere bedingte Ausdrücke wie false?f():g() , wo f/g() Typinferenz benötigt.

Die Linie wurde zwischen primitiven und Referenztypen gezeichnet. In op1?op2:op3 , wenn sowohl op2 als auch op3 "eindeutig" primitive Typen (oder geboxte Versionen von) sind, wird dies als eigenständig behandelt. Zitieren Dan Smith -

  

Wir klassifizieren hier bedingte Ausdrücke, um die Typisierungsregeln von Referenzbedingungen (15.25.3) zu verbessern und gleichzeitig das Verhalten boolescher und numerischer Bedingungen zu erhalten. Wenn wir versuchen würden, alle Bedingungen einheitlich zu behandeln, gäbe es eine Vielzahl unerwünschter inkompatibler Änderungen, einschließlich Änderungen in der Überladungsauflösung und beim Boxing / Unboxing-Verhalten.

In Ihrem Fall

%Vor%

da false?4:null "eindeutig" (?) ein Integer ist, hat der übergeordnete Ausdruck die Form ?:int:Integer ; Dies ist ein primitiver Fall, und sein Verhalten wird mit java7, also NPE, kompatibel gehalten.

Ich ziehe Zitate "klar", weil das mein intuitives Verständnis ist; Ich bin mir nicht sicher über die formale Spezifikation. Schauen wir uns dieses Beispiel an

%Vor%

Es kompiliert! und es gibt keine NPE zur Laufzeit! Ich weiß nicht, wie ich die Spezifikationen in diesem Fall befolgen soll. Ich kann mir vorstellen, dass der Compiler wahrscheinlich die folgenden Schritte ausführt:

1) Unterausdruck false?f1():null ist nicht "eindeutig" ein (eingerahmter) primitiver Typ; sein Typ ist noch unbekannt

2) Daher wird der übergeordnete Ausdruck als "bedingter Ausdruck" klassifiziert, der in einem Zuordnungskontext erscheint.

3) Der Zieltyp Integer wird auf die Operanden angewendet und schließlich auf f1() , woraus sich ergibt, dass Integer

zurückgegeben wird

4) Wir können jedoch jetzt nicht zurückgehen, um den bedingten Ausdruck als ?int:Integer zu klassifizieren.

Das klingt vernünftig. Was aber, wenn wir das Argument type für f1() ? Explizit angeben?

%Vor%

Theorie (A) - Dies sollte die Semantik des Programms nicht verändern, da es sich um das Argument gleichen Typs handelt, das man hätte ableiten können. Wir sollten NPE zur Laufzeit nicht sehen.

Theorie (B) - es gibt keine Typinferenz; der Typ des Unterausdrucks ist eindeutig Integer , daher sollte dies als primitiver Fall klassifiziert werden, und wir sollten NPE zur Laufzeit sehen.

Ich glaube an (B); javac (8u60) macht jedoch (A). Ich verstehe nicht warum.

Diese Beobachtung auf eine urkomische Ebene schieben

%Vor%

Das macht keinen Sinn; etwas wirklich funky geht in javac.

(Siehe auch Java-Autoboxing und ternären Operator-Wahnsinn )

    
ZhongYu 17.10.2015 02:24
quelle

Tags und Links