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?


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


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; 

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).

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.