Booleanos, operadores condicionais e caixa automática

132

Por que isso joga 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;
}

enquanto isso não

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

?

A solução é pela maneira a substituir falsepor Boolean.FALSEevitar nullsendo desemoldurado para boolean--que não é possível. Mas essa não é a questão. A questão é por quê ? Há alguma referência no JLS que confirme esse comportamento, especialmente no 2º caso?

BalusC
fonte
28
uau, o autoboxing é uma fonte infinita de ... er ... surpresas para o programador java, não é? :-) #
31410 leonbloy
Eu tive um problema semelhante e o que me surpreendeu foi que ele falhou na VM do OpenJDK, mas funcionou na VM do HotSpot ... Escreva uma vez, execute em qualquer lugar!
Kodu

Respostas:

92

A diferença é que o tipo explícito do returnsNull()método afeta a digitação estática das expressões no tempo de compilação:

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

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

Consulte Especificação da linguagem Java, seção 15.25 Operador condicional? :

  • Para E1, os tipos dos segundo e terceiro operandos são Booleane booleanrespectivamente, portanto, esta cláusula se aplica:

    Se um dos segundo e terceiro operandos for do tipo booleano e o tipo do outro for do tipo booleano, o tipo da expressão condicional será booleano.

    Como o tipo da expressão é boolean, o segundo operando deve ser coagido para boolean. O compilador insere o código de descompactação automática no 2º operando (valor de retorno de returnsNull()) para fazê-lo digitar boolean. Obviamente, isso faz com que o NPE nullretorne no tempo de execução.

  • Para E2, os tipos do 2º e 3º operandos são <special null type>(não Booleancomo em E1!) E booleanrespectivamente, portanto, nenhuma cláusula de digitação específica se aplica ( leia-os! ); Portanto, a cláusula final "caso contrário" se aplica:

    Caso contrário, o segundo e o terceiro operandos são do tipo S1 e S2, respectivamente. Seja T1 o tipo resultante da aplicação da conversão de boxe em S1 e T2 seja o tipo resultante da aplicação da conversão de boxe em S2. O tipo da expressão condicional é o resultado da aplicação da conversão de captura (§5.1.10) em lub (T1, T2) (§15.12.2.7).

    • S1 == <special null type>(ver §4.1 )
    • S2 == boolean
    • T1 == caixa (S1) == <special null type>(veja o último item na lista de conversões de boxe em §5.1.7 )
    • T2 == caixa (S2) == `Boolean
    • lub (T1, T2) == Boolean

    Portanto, o tipo da expressão condicional é Booleane o terceiro operando deve ser coagido Boolean. O compilador insere o código de boxe automático para o terceiro operando ( false). O segundo operando não precisa do desempacotamento automático como em E1, portanto, nenhum NPE de desempacotamento automático nullé retornado.


Esta pergunta precisa de uma análise de tipo semelhante:

Operador condicional Java?: Tipo de resultado

Bert F
fonte
4
Faz sentido ... eu acho. O §15.12.2.7 é uma dor.
BalusC
É fácil ... mas apenas em retrospectiva. :-)
Bert F
@BertF O que faz a função lubde lub(T1,T2)suporte para?
25414 Geek
1
@Geek - lub () - menos limite superior - basicamente a superclasse mais próxima que eles têm em comum; como null (tipo "o tipo nulo especial") pode ser implicitamente convertido (ampliado) para qualquer tipo, você pode considerar o tipo nulo especial como uma "superclasse" de qualquer tipo (classe) para os fins de lub ().
Bert F
25

A linha:

    Boolean b = true ? returnsNull() : false;

é transformado internamente para:

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

executar o unboxing; assim: null.booleanValue()produzirá um NPE

Essa é uma das principais armadilhas ao usar o autoboxing. Esse comportamento é realmente documentado no 5.1.8 JLS

Edit: Eu acredito que o unboxing é devido ao terceiro operador ser do tipo booleano, como (elenco implícito adicionado):

   Boolean b = (Boolean) true ? true : false; 
jjungnickel
fonte
2
Por que ele tenta desmarcar assim, quando o valor final é um objeto booleano?
Erick Robertson
16

Na especificação da linguagem Java, seção 15.25 :

  • Se um dos segundo e terceiro operandos for do tipo booleano e o tipo do outro for do tipo booleano, o tipo da expressão condicional será booleano.

Portanto, o primeiro exemplo tenta chamar Boolean.booleanValue()para converter Booleanpara booleanconforme a primeira regra.

No segundo caso, o primeiro operando é do tipo nulo, quando o segundo não é do tipo de referência, portanto, a conversão de caixa automática é aplicada:

  • Caso contrário, o segundo e o terceiro operandos são do tipo S1 e S2, respectivamente. Seja T1 o tipo resultante da aplicação da conversão de boxe em S1 e T2 seja o tipo resultante da aplicação da conversão de boxe em S2. O tipo da expressão condicional é o resultado da aplicação da conversão de captura (§5.1.10) em lub (T1, T2) (§15.12.2.7).
axtavt
fonte
Isso responde ao primeiro caso, mas não ao segundo.
BalusC
Provavelmente, há uma exceção para quando um dos valores é null.
Erick Robertson
@Erick: JLS confirma isso?
BalusC
1
@ Erick: Eu não acho que seja aplicável, pois booleannão é um tipo de referência.
axtavt
1
E devo acrescentar ... é por isso que você deve tornar ambos os lados de um ternário do mesmo tipo, com chamadas explícitas, se necessário. Mesmo que você tenha as especificações memorizadas e saiba o que acontecerá, o próximo programador a ler o seu código talvez não. Na minha humilde opinião, seria melhor se o compilador apenas produzisse uma mensagem de erro nessas situações, em vez de fazer coisas difíceis para os mortais comuns preverem. Bem, talvez haja casos em que o comportamento é realmente útil, mas ainda não o atingi.
Jay
0

Podemos ver esse problema no código de bytes. Na linha 3 do código de bytes principal 3: invokevirtual #3 // Method java/lang/Boolean.booleanValue:()Z, o booleano de box de valor nulo, invokevirtualo método java.lang.Boolean.booleanValue, lançará o NPE, é claro.

    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
Yanhui Zhou
fonte