Booleani, operatori condizionali e autoboxing


132

Perché questo lancio NullPointerException

public static void main(String[] args) throws Exception {
    Boolean b = true ? returnsNull() : false; // NPE on this line.
    System.out.println(b);
}

public static Boolean returnsNull() {
    return null;
}

mentre questo non lo fa

public static void main(String[] args) throws Exception {
    Boolean b = true ? null : false;
    System.out.println(b); // null
}

?

La soluzione è il modo per sostituire falseper Boolean.FALSEevitare nulldi essere unboxed a boolean--che non è possibile. Ma questa non è la domanda. La domanda è: perché ? Ci sono riferimenti in JLS che confermano questo comportamento, in particolare del secondo caso?


28
wow, l'autoboxing è una fonte infinita di ... ehm ... sorprese per il programmatore Java, non è vero? :-)
leonbloy,

Ho avuto un problema simile e ciò che mi ha sorpreso è che non è riuscito sulla VM OpenJDK ma ha funzionato sulla VM HotSpot ... Scrivi una volta, esegui ovunque!
Kodu,

Risposte:


92

La differenza è che il tipo esplicito del returnsNull()metodo influenza la tipizzazione statica delle espressioni in fase di compilazione:

E1: `true ? returnsNull() : false` - boolean (auto-unboxing 2nd operand to boolean)

E2: `true ? null : false` - Boolean (autoboxing of 3rd operand to Boolean)

Vedere le specifiche del linguaggio Java, sezione 15.25 Operatore condizionale? :

  • Per E1, i tipi del 2o e 3o operando sono Booleane booleanrispettivamente, quindi questa clausola si applica:

    Se uno dei secondi e terzi operandi è di tipo booleano e il tipo dell'altro è di tipo booleano, il tipo dell'espressione condizionale è booleano.

    Poiché il tipo di espressione è boolean, il 2 ° operando deve essere forzato boolean. Il compilatore inserisce il codice di unboxing automatico nel 2 ° operando (valore di ritorno di returnsNull()) per renderlo di tipo boolean. Ciò ovviamente causa l'NPE dal nullreso in fase di esecuzione.

  • Per E2, i tipi del 2o e 3o operando sono <special null type>(non Booleancome in E1!) E booleanrispettivamente, quindi non si applica alcuna specifica clausola di battitura ( vai a leggere! ), Quindi si applica la clausola finale "altrimenti":

    Altrimenti, il secondo e il terzo operando sono rispettivamente di tipo S1 e S2. Sia T1 il tipo che risulta dall'applicazione della conversione di boxe a S1 e T2 sia il tipo risultante dall'applicazione della conversione di boxe a S2. Il tipo dell'espressione condizionale è il risultato dell'applicazione della conversione della cattura (§5.1.10) in lub (T1, T2) (§15.12.2.7).

    • S1 == <special null type>(vedi §4.1 )
    • S2 == boolean
    • T1 == box (S1) == <special null type>(vedi l'ultimo elemento nell'elenco delle conversioni di boxe in §5.1.7 )
    • T2 == box (S2) == `Booleano
    • lub (T1, T2) == Boolean

    Quindi il tipo dell'espressione condizionale è Booleane il 3o operando deve essere forzato Boolean. Il compilatore inserisce il codice di auto-boxing per il 3o operando ( false). Il 2o operando non ha bisogno dell'auto-unboxing come in E1, quindi nessun NPE auto-unboxing quando nullviene restituito.


Questa domanda richiede un'analisi del tipo simile:

Operatore condizionale Java?: Tipo di risultato


4
Ha senso ... penso. Il §15.12.2.7 è un dolore.
BalusC

È facile ... ma solo col senno di poi. :-)
Bert F

@BertF Cosa fa la funzione lubdi lub(T1,T2)supporto per?
Geek,

1
@Geek - lub () - limite superiore minimo - sostanzialmente la superclasse più vicina che hanno in comune; poiché null (tipo "il tipo null speciale") può essere implicitamente convertito (ampliato) in qualsiasi tipo, è possibile considerare il tipo null speciale come una "superclasse" di qualsiasi tipo (classe) ai fini di lub ().
Bert F,

25

La linea:

    Boolean b = true ? returnsNull() : false;

è trasformato internamente in:

    Boolean b = true ? returnsNull().booleanValue() : false; 

per eseguire il unboxing; quindi: null.booleanValue()produrrà un NPE

Questa è una delle principali insidie ​​quando si utilizza l'autoboxing. Questo comportamento è infatti documentato in JLS 5.1.8

Modifica: credo che il unboxing sia dovuto al fatto che il terzo operatore sia di tipo booleano, come (cast implicito aggiunto):

   Boolean b = (Boolean) true ? true : false; 

2
Perché prova a decomprimere in questo modo, quando il valore finale è un oggetto booleano?
Erick Robertson,

16

Dalle specifiche del linguaggio Java, sezione 15.25 :

  • Se uno dei secondi e terzi operandi è di tipo booleano e il tipo dell'altro è di tipo booleano, il tipo dell'espressione condizionale è booleano.

Così, il primo esempio tenta di chiamare Boolean.booleanValue()per convertire Booleanal booleansecondo la prima regola.

Nel secondo caso il primo operando è di tipo null, quando il secondo non è di tipo di riferimento, quindi viene applicata la conversione di autoboxing:

  • Altrimenti, il secondo e il terzo operando sono rispettivamente di tipo S1 e S2. Sia T1 il tipo che risulta dall'applicazione della conversione di boxe a S1 e T2 sia il tipo risultante dall'applicazione della conversione di boxe a S2. Il tipo dell'espressione condizionale è il risultato dell'applicazione della conversione della cattura (§5.1.10) in lub (T1, T2) (§15.12.2.7).

Questo risponde al primo caso, ma non al secondo caso.
BalusC

Probabilmente c'è un'eccezione per quando uno dei valori è null.
Erick Robertson,

@Erick: JLS lo conferma?
BalusC

1
@Erick: non credo sia applicabile poiché booleannon è un tipo di riferimento.
axtavt,

1
E posso aggiungere ... ecco perché dovresti rendere entrambi i lati di un ternario dello stesso tipo, con chiamate esplicite se necessario. Anche se hai memorizzato le specifiche e sai cosa accadrà, il prossimo programmatore che arriva e legge il tuo codice potrebbe non farlo. Secondo la mia modesta opinione, sarebbe meglio se il compilatore avesse appena prodotto un messaggio di errore in queste situazioni piuttosto che fare cose difficili da prevedere per i comuni mortali. Bene, forse ci sono casi in cui il comportamento è veramente utile, ma non ne ho ancora colpito uno.
Jay,

0

Possiamo vedere questo problema dal codice byte. Alla riga 3 del codice byte di main 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Z, il valore booleano boxe di valore null, invokevirtualil metodo java.lang.Boolean.booleanValue, genererà naturalmente NPE.

    public static void main(java.lang.String[]) throws java.lang.Exception;
      descriptor: ([Ljava/lang/String;)V
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=2, locals=2, args_size=1
           0: invokestatic  #2                  // Method returnsNull:()Ljava/lang/Boolean;
           3: invokevirtual #3                  // Method java/lang/Boolean.booleanValue:()Z
           6: invokestatic  #4                  // Method java/lang/Boolean.valueOf:(Z)Ljava/lang/Boolean;
           9: astore_1
          10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
          13: aload_1
          14: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
          17: return
        LineNumberTable:
          line 3: 0
          line 4: 10
          line 5: 17
      Exceptions:
        throws java.lang.Exception

    public static java.lang.Boolean returnsNull();
      descriptor: ()Ljava/lang/Boolean;
      flags: ACC_PUBLIC, ACC_STATIC
      Code:
        stack=1, locals=0, args_size=0
           0: aconst_null
           1: areturn
        LineNumberTable:
          line 8: 0
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.