2015-10-16 12 views
8

Uno dei miei studenti riceve un'eccezione di puntatore nullo quando utilizza un operatore ternario che a volte risulta nullo. Penso di aver capito il problema, ma sembra che sia il risultato di un'inferenza di tipo incoerente. O per dirla in altro modo, mi sento come se la semantica qui fosse incoerente e l'errore dovrebbe essere evitabile senza cambiare il suo approccio.L'operatore ternario Java sembra eseguire incoerentemente il lancio di interi integer

Questa domanda è simile a, ma diversa da Another question about ternary operators. In questa domanda, il numero intero intero deve essere forzato a un int perché il valore di ritorno della funzione è un int. Tuttavia, questo non è il caso nel codice del mio studente.

Questo codice funziona benissimo:

Integer x = (5>7) ? 3 : null; 

Il valore di x è nullo. Nessun NPE. In questo caso, il compilatore può capire che il risultato dell'operatore ternario deve essere Integer, quindi inserisce il 3 (un int) in un intero invece di trasmettere null a int.

Tuttavia, l'esecuzione di questo codice:

Integer x = (5>7) ? 3 : (5 > 8) ? 4 : null; 

si traduce in un NPE. L'unica ragione che potrebbe accadere è che il valore null viene eseguito su un int, ma ciò non è realmente necessario e sembra incoerente con il primo bit di codice. Cioè, se il compilatore può dedurre per il primo snipet che il risultato dell'operatore ternario è un intero, perché non può farlo nel secondo caso? Il risultato della seconda espressione ternaria deve essere un numero intero, e poiché tale risultato è il secondo risultato per il primo operatore ternario, anche il risultato del primo operatore ternario dovrebbe essere un numero intero.

Un'altra snipet funziona bene:

Integer three = 3; 

Integer x = (5>7) ? three : (5 > 8) ? three+1 : null; 

Qui, il compilatore sembra essere in grado di dedurre che il risultato di entrambi gli operatori ternari è un numero intero e quindi non forzare pressofuso il nulla a int.

+0

Look up autoboxing. –

+0

Accesso puntatore nullo: questa espressione di tipo Integer è nullo ma richiede l'auto-unboxing –

+0

Vedere anche queste domande: [Autoboxing Java e follia ternaria] (http://stackoverflow.com/questions/25417438/java-autoboxing-and-ternary -operator-madness) e [NPE tramite auto-boxing del ternario Java] (http://stackoverflow.com/questions/12763983/nullpointerexception-through-auto-boxing-behavior-of-java-ternary-operator). –

risposta

10

La chiave per questo è che l'operatore condizionale è giusto associativo. Le regole per determinare il tipo di risultato di un'espressione condizionale sono hideously complicated ma si riduce a questo:

  1. Prima (5 > 8) ? 4 : null viene valutato, il 2 ° operando è un int, il 3 ° è null, se guardiamo in su nel tabella, il tipo di risultato di questa espressione è Integer. (In altre parole: perché uno degli operandi è null, questa viene trattata come una riferimento espressione condizionale)
  2. Poi abbiamo (5>7) ? 3 : <previous result> valutare, il che significa che nella tabella legata soprattutto, è necessario ricercare il Tipo di risultato per 2o operando int e 3o operando Integer: ed è int. Ciò significa che <previous result> deve essere disattivato e non riesce con uno NPE.

Quindi perché funziona il primo caso?

Ci abbiamo (5>7) ? 3 : null;, come abbiamo visto, se 2 ° operando è int e 3 ° è null, il tipo di risultato è Integer.Ma lo assegniamo a una variabile del tipo Integer, quindi non è necessario unboxing.

Tuttavia ciò avviene solo con la null letterale, il codice sarà ancora gettare un NPE, perché tipi di operando int e Integer risultato in una numerico espressione condizionale:

Integer i = null; 
Integer x = (5>7) ? 3 : i; 

Riassumendo: NON è una specie di logica, ma non è la logica umana.

  1. Se entrambi gli operandi sono di tipo compilato Integer, il risultato è un Integer.
  2. Se un operando è di tipo int e l'altro è Integer, il risultato è uno int.
  3. Se un operando è di null type (di cui l'unico valore valido è il riferimento null), il risultato è un Integer.
+0

Bel riassunto. Qualche idea sul perché quelle tabelle nel JLS specificano che '? int: Integer' e '? Integer: int'map to int piuttosto che Integer? –

+0

@AndyThomas Si può solo indovinare, ma penso che l'idea fosse che la maggior parte delle volte si assegnasse il risultato a una variabile 'int' in ogni caso, e creando un'istanza' Integer' in più per il risultato giusto per scartarla subito è stato visto come troppo costoso. Ma questo non è altro che un'ipotesi, potrebbero esserci altre ragioni più valide. – biziclop

+0

Ok, perché è un int ma nel primo esempio che ha la stessa forma di la risposta è intero? –

1
int x = (int) (5>7) ? 3 : null; 

risultati in NPE poiché nulla è realizzata mediante fusione a int.

stesso con il vostro codice di

Integer x = (5>7) ? 3 : (5 > 8) ? 4 : null; 

assomiglia a questo:

Integer x = (Integer) ((5>7) ? (int) 3 : (5 > 8) ? 4 : null); 

Come si vede nulla di nuovo è colato a int, che non riesce.

Per risolvere questo si potrebbe fare questo:

Integer x = (Integer) ((5>7) ? (Integer) 3 : (5 > 8) ? 4 : null); 

Ora non tenta di gettare nulla a int, ma Integer.

0

Prima di Java8, in quasi tutti i casi & dagger;, il tipo di un'espressione è creato dal basso verso l'alto, interamente in base ai tipi di sottoespressioni; non dipende dal contesto. Questo è bello e semplice, e il codice è facile da capire; ad esempio, la risoluzione del sovraccarico dipende dai tipi di argomenti, che vengono risolti indipendentemente dal contesto di richiamo del metodo.

(& pugnale; L'unica eccezione che so è jls#15.12.2.8)

Dato un'espressione condizionale in forma di ?int:Integer, spec deve definire un tipo fisso per esso senza riguardo al contesto. È stato scelto il tipo int, che è presumibilmente migliore nella maggior parte dei casi d'uso. Ovviamente, è anche la fonte di NPE da unboxing.


In Java8, le informazioni sul tipo contestuale possono essere utilizzate per l'inferenza di tipo. Questo è conveniente per molti casi; ma introduce anche confusione, poiché potrebbero esserci due direzioni per risolvere il tipo di un'espressione. Fortunatamente, alcune espressioni sono ancora autonome; i loro tipi sono indipendenti dal contesto.

con.t espressioni condizionali, non vogliamo che quelli semplici come false?0:1 siano dipendenti dal contesto; i loro tipi sono evidenti. D'altra parte, vogliamo l'inferenza di tipo contestuale su espressioni condizionali più complicate, come false?f():g() dove f/g() richiede l'inferenza di tipo.

La linea è stata tracciata tra i tipi primitivi e di riferimento. In op1?op2:op3, se sia op2 e op3 sono tipi "chiari" primitivi (o versioni in scatola di), viene considerato come autonomo. Quoting Dan Smith -

classifichiamo espressioni condizionali qui al fine di migliorare le regole di battitura di condizionali di riferimento (15.25.3), preservando il comportamento attuale dei condizionali booleani e numerici. Se provassimo a trattare tutti i condizionali in modo uniforme, ci sarebbero una varietà di modifiche incompatibili non volute, comprese le modifiche alla risoluzione di sovraccarico e al comportamento di boxe/unboxing.

Nel tuo caso

Integer x = false ? 3 : false ? 4 : null; 

dal false?4:null è "chiaramente" un Integer, l'espressione genitore è in forma di ?:int:Integer (?); questo è un caso primitivo, e il suo comportamento è mantenuto compatibile con java7, quindi, NPE.


Ho messo le virgolette su "chiaramente" perché questa è la mia comprensione intuitiva; Non sono sicuro delle specifiche formali. Diamo un'occhiata a questo esempio

static <T> T f1(){ return null; } 
-- 

Integer x = false ? 3 : false ? f1() : null; 

Compila! e non c'è NPE in fase di esecuzione! Non so come seguire le specifiche su questo caso. Posso immaginare che il compilatore faccia probabilmente i seguenti passi:

1) la sottoespressione false?f1():null non è "chiaramente" un tipo primitivo (in scatola); il suo tipo è ancora sconosciuto

2) quindi, l'espressione genitore è classificata come "espressione condizionale di riferimento", che appare in un contesto di assegnazione.

3) il tipo di destinazione Integer viene applicato agli operandi, ed eventualmente f1(), che viene poi dedotta per tornare Integer

4) Tuttavia, non possiamo tornare indietro a ri-classificare l'espressione condizionale come ?int:Integer .


Sembra ragionevole. Tuttavia, cosa succede se si specifica esplicitamente l'argomento type su f1()? Teoria

Integer x = false ? 3 : false ? Test.<Integer>f1() : null; 

(A) - questo non dovrebbe alterare la semantica del programma, perché è lo stesso tipo di argomento che sarebbe stato dedotto. Non dovremmo vedere NPE in fase di runtime.

Teoria (B) - non esiste un'inferenza di tipo; il tipo di sottoespressione è chiaramente Integer, quindi questo dovrebbe essere classificato come caso primitivo e dovremmo vedere NPE in fase di esecuzione.

Credo in (B); tuttavia, javac (8u60) fa (A). Non capisco perché.


Spingendo questa osservazione ad un livello esilarante

class MyList1 extends ArrayList<Integer> 
    { 
     //inherit public Integer get(int index) 
    } 

    class MyList2 extends ArrayList<Integer> 
    { 
     @Override public Integer get(int index) 
     { 
      return super.get(0); 
     } 
    } 

    MyList1 myList1 = new MyList1(); 
    MyList2 myList2 = new MyList2(); 

    Integer x1 = false ? 3 : false ? myList1.get(0) : null; // no NPE 
    Integer x2 = false ? 3 : false ? myList2.get(0) : null; // NPE !!! 

Ciò non ha alcun senso; qualcosa di veramente funky sta succedendo all'interno di javac.

(vedi anche Java autoboxing and ternary operator madness)

+0

lezione appresa - Usa sempre i tipi espliciti, identici in 'op2' e' op3'. Non mischiare mai 'int' e' Integer' qui; fare inscatolamento/unboxing esplicito manuale se necessario. Non mischiare mai 'int' e' long' ecc qui; effettuare la conversione manuale del tipo primitivo, se necessario. Questa parte della specifica è troppo inaffidabile. – ZhongYu