Retornando null como um int permitido com operador ternário, mas não if

186

Vejamos o código Java simples no seguinte trecho:

public class Main {

    private int temp() {
        return true ? null : 0;
        // No compiler error - the compiler allows a return value of null
        // in a method signature that returns an int.
    }

    private int same() {
        if (true) {
            return null;
            // The same is not possible with if,
            // and causes a compile-time error - incompatible types.
        } else {
            return 0;
        }
    }

    public static void main(String[] args) {
        Main m = new Main();
        System.out.println(m.temp());
        System.out.println(m.same());
    }
}

Neste código Java mais simples, o temp()método não emite nenhum erro do compilador, mesmo que o tipo de retorno da função seja int, e estamos tentando retornar o valor null(por meio da instrução return true ? null : 0;). Quando compilado, isso obviamente causa a exceção do tempo de execução NullPointerException.

No entanto, parece que a mesma coisa está errada, se nós representamos o operador ternário com uma ifdeclaração (como no same()método), que faz emitir um erro de compilação! Por quê?

Leão
fonte
6
Além disso, int foo = (true ? null : 0)e new Integer(null)ambos compilam bem, o segundo é a forma explícita de autoboxing.
Izkata
2
@Izkata o problema aqui é para mim entender por que o compilador está tentando autobox nullpara Integer... Isso seria olhar apenas como "adivinhar" a mim ou "fazer as coisas funcionarem" ...
Marsellus Wallace
1
... Huhm, eu pensei que tinha uma resposta lá, pois o construtor Inteiro (o que os documentos que encontrei dizem que é usado para autoboxing) tem permissão para usar uma String como argumento (que pode ser nulo). No entanto, eles também dizem que o construtor age de forma idêntica para o parseInt () método, que jogaria um NumberFormatException Ao chegar passou um nulo ...
Izkata
3
@ Izkata - o argumento String para o número inteiro não é uma operação de autoboxing. Uma String não pode ser autoboxada para um Inteiro. (A função Integer foo() { return "1"; }não irá compilar.)
Ted Hopp
5
Legal, aprendeu algo novo sobre o operador ternário!
oksayt

Respostas:

118

O compilador interpreta nullcomo uma referência nula a Integer, aplica as regras de caixa automática / unboxing para o operador condicional (conforme descrito na Java Language Specification, 15.25 ) e segue em frente. Isso gerará um NullPointerExceptiontempo de execução, que você pode confirmar tentando.

Ted Hopp
fonte
Dado o link para a Especificação de linguagem Java que você postou, qual ponto você acha que é executado no caso da pergunta acima? O último (já que ainda estou tentando entender capture conversione lub(T1,T2)) ?? Além disso, é realmente possível aplicar boxe a um valor nulo? Isso não seria como "adivinhar"?
Marsellus Wallace
´ @ Gevorg Um ponteiro nulo é um ponteiro válido para todos os objetos possíveis, para que nada de ruim possa acontecer lá. O compilador apenas assume que nulo é um número inteiro que, em seguida, pode autobox para int.
Voo
1
@Gevorg - Veja o comentário do nowaq e minha resposta ao seu post. Eu acho que ele escolheu a cláusula correta. lub(T1,T2)é o tipo de referência mais específico em comum na hierarquia de tipos de T1 e T2. (Ambos compartilham pelo menos Object, por isso há sempre um tipo de referência mais específica.)
Ted Hopp
8
@ Gevorg - nullnão é encaixotado em um número inteiro, é interpretado como uma referência a um número inteiro (uma referência nula, mas isso não é um problema). Nenhum objeto Inteiro é construído a partir do nulo; portanto, não há motivo para uma NumberFormatException.
Ted Hopp
1
@Gevorg - Se você observar as regras para conversão de boxe e aplicá-las a null(que não é um tipo numérico primitivo), a cláusula aplicável será "Se p for um valor de qualquer outro tipo, a conversão de boxe será equivalente a uma conversão de identidade. " Portanto, conversão de boxe nullpara Integeryields null, sem chamar nenhum Integerconstrutor.
Ted Hopp
40

Eu acho que o compilador Java interpreta true ? null : 0como uma Integerexpressão, que pode ser implicitamente convertida em int, possivelmente dando NullPointerException.

Para o segundo caso, a expressão nullé do tipo nulo especial , consulte , portanto, o código return nullcria incompatibilidade de tipo.

Vlad
fonte
2
Suponho que isso esteja relacionado ao boxe automático? Presumivelmente, o primeiro retorno não seria compilado antes do Java 5, certo?
22711 Michael Jackson
@ Michael, que parece ser o caso, se você definir o nível de conformidade do Eclipse como pré-5.
Jonathon Faust
@ Michael: isso definitivamente se parece com o boxe automático (eu sou bastante novo em Java e não posso fazer declarações mais definidas - desculpe).
Vlad
1
@ Vlad: como o compilador acabaria interpretando true ? null : 0como Integer? Autoboxing 0primeiro ??
Marcelus Wallace
1
@ Gevorg: Veja aqui : 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. e o texto a seguir.
117411 Vlad
32

Na verdade, tudo está explicado na Especificação de Linguagem Java .

O tipo de uma expressão condicional é determinado da seguinte maneira:

  • Se o segundo e o terceiro operandos tiverem o mesmo tipo (que pode ser o tipo nulo), esse é o tipo da expressão condicional.

Portanto, o "nulo" no seu (true ? null : 0)obtém um tipo int e é autoboxado como Inteiro.

Tente algo assim para verificar isso (true ? null : null)e você receberá o erro do compilador.

nowaq
fonte
3
Mas essa cláusula das regras não se aplica: o segundo e o terceiro operandos não têm o mesmo tipo.
Ted Hopp
1
Então a resposta parece estar na seguinte declaração:> 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).
nowaq
Eu acho que é a cláusula aplicável. Em seguida, ele tenta aplicar o desempacotamento automático para retornar um intvalor da função, o que causa um NPE.
quer
@nowaq eu pensei isso também. No entanto, se você tentar caixa explicitamente nullpara Integercom new Integer(null);"Let T1 ser o tipo que resulta da aplicação de conversão de boxe para S1 ..." você teria um NumberFormatExceptione este não é o caso ...
Marsellus Wallace
@ Gevorg Eu acho que desde que uma exceção acontece ao fazer o boxe, não obtemos nenhum resultado aqui. O compilador é obrigado a gerar código que segue a definição que ele faz - nós apenas obtemos a exceção antes de terminar.
Voo
25

No caso da ifdeclaração, a nullreferência não é tratada como Integerreferência porque não está participando de uma expressão que a força a ser interpretada como tal. Portanto, o erro pode ser facilmente capturado em tempo de compilação, porque é mais claramente um erro de tipo .

Quanto ao operador condicional, a Especificação da Linguagem Java §15.25 "Operador Condicional ? :" responde muito bem a isso nas regras de como a conversão de tipo é aplicada:

  • Se o segundo e o terceiro operandos tiverem o mesmo tipo (que pode ser o tipo nulo), esse é o tipo da expressão condicional.

    Não se aplica porque nullnão é int.

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

    Não se aplica porque nem nullnem inté booleanou Boolean.

  • Se um dos segundo e terceiro operandos for do tipo nulo e o tipo do outro for um tipo de referência, o tipo da expressão condicional será esse tipo de referência.

    Não se aplica porque nullé do tipo nulo, mas intnão é um tipo de referência.

  • Caso contrário, se o segundo e o terceiro operando tiverem tipos conversíveis (§5.1.8) em tipos numéricos, existem vários casos: […]

    Aplica-se: nullé tratado como conversível em um tipo numérico e é definido em §5.1. 8 "Conversão de Unboxing" para lançar a NullPointerException.
Jon Purdy
fonte
Se 0for autoboxado Integer, o compilador está executando o último caso das "regras de operador ternárias", conforme descrito na Especificação de Linguagem Java. Se isso for verdade, é difícil para mim acreditar que ele passaria para o caso 3 das mesmas regras que possuem um tipo nulo e de referência que fazem com que o valor de retorno do operador ternário seja o tipo de referência (Inteiro). .
Marsellus Wallace
@ Gevorg - Por que é difícil acreditar que o operador ternário está retornando um Integer? É exatamente o que está acontecendo; o NPE está sendo gerado tentando desmarcar o valor da expressão para retornar um intda função Altere a função para retornar um Integere ele retornará nullsem problemas.
Ted Hopp
2
@TedHopp: Gevorg estava respondendo a uma revisão anterior da minha resposta, que estava incorreta. Você deve ignorar a discrepância.
Jon Purdy
@JonPurdy "Diz-se que um tipo é conversível em um tipo numérico, se for do tipo numérico ou é um tipo de referência que pode ser convertido em um tipo numérico pela conversão de unboxing" e não acho que se nullenquadre nessa categoria . Além disso, entraríamos na etapa "Caso contrário, a promoção numérica binária (§5.6.2) é aplicada ... Observe que a promoção numérica binária executa a conversão de conversão de unboxing (§5.1.8) ..." para determinar o tipo de retorno. Mas a conversão de unboxing geraria um NPE e isso acontece apenas em tempo de execução e não ao tentar determinar o tipo de operador ternário. Eu ainda estou confuso ..
Marsellus Wallace
@ Gevorg: Unboxing acontece em tempo de execução. O nullé tratado como se tivesse tipo int, mas é realmente equivalente a throw new NullPointerException(), só isso.
Jon Purdy
11

A primeira coisa a ter em mente é que os operadores ternários Java têm um "tipo", e é isso que o compilador determinará e considerará, independentemente dos tipos reais / reais do segundo ou terceiro parâmetro. Dependendo de vários fatores, o tipo de operador ternário é determinado de diferentes maneiras, conforme ilustrado na Java Language Specification 15.26

Na pergunta acima, devemos considerar o último caso:

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

Esse é, de longe, o caso mais complexo, depois de aplicar a conversão de captura (§5.1.10) e, principalmente, em lub (T1, T2) .

Em inglês simples e após uma extrema simplificação, podemos descrever o processo como o cálculo da "Superclasse Menos Comum" (sim, pense no LCM) do segundo e terceiro parâmetros. Isso nos dará o operador ternário "tipo". Novamente, o que acabei de dizer é uma simplificação extrema (considere as classes que implementam várias interfaces comuns).

Por exemplo, se você tentar o seguinte:

long millis = System.currentTimeMillis();
return(true ? new java.sql.Timestamp(millis) : new java.sql.Time(millis));

Você notará que o tipo resultante da expressão condicional é java.util.Dateuma vez que é a "Superclasse Menos Comum" do par Timestamp/ Time.

Como nullpode ser autoboxed para qualquer coisa, a "Superclasse Menos Comum" é a Integerclasse e este será o tipo de retorno da expressão condicional (operador ternário) acima. O valor de retorno será um ponteiro nulo do tipo Integere é isso que será retornado pelo operador ternário.

Em tempo de execução, quando a Máquina Virtual Java unboxes a Integerum NullPointerExceptioné lançada. Isso acontece porque a JVM tenta chamar a função null.intValue(), onde nullé o resultado da caixa automática.

Na minha opinião (e como minha opinião não está na Especificação de linguagem Java, muitas pessoas acharão errado de qualquer maneira), o compilador faz um mau trabalho ao avaliar a expressão em sua pergunta. Dado que você escreveu, true ? param1 : param2o compilador deve determinar imediatamente que o primeiro parâmetro - null- será retornado e deverá gerar um erro no compilador. Isso é um pouco parecido com quando você escreve while(true){} etc...e o compilador reclama do código abaixo do loop e o sinaliza Unreachable Statements.

Seu segundo caso é bem direto e essa resposta já é muito longa ...;)

CORREÇÃO:

Depois de outra análise, acredito que errei ao dizer que um nullvalor pode ser colocado em caixa / caixa automática para qualquer coisa. Falando sobre a classe Inteiro, o boxe explícito consiste em invocar o new Integer(...)construtor ou talvez o Integer.valueOf(int i);(encontrei esta versão em algum lugar). O primeiro jogaria um NumberFormatException(e isso não acontece) enquanto o segundo simplesmente não faria sentido, pois um intnão pode ser null...

Marsellus Wallace
fonte
1
O nullcódigo original do OP não está em caixa. A maneira como funciona é: o compilador assume que nullé uma referência a um número inteiro. Usando as regras para tipos de expressões ternárias, ele decide que a expressão inteira é uma expressão Inteira. Em seguida, ele gera código para autoboxar o 1(caso a condição seja avaliada como false). Durante a execução, a condição é avaliada para trueque a expressão seja avaliada null. Ao tentar retornar um intda função, o nullestá fora da caixa. Isso lança um NPE. (O compilador pode otimizar a maioria desta distância.)
Ted Hopp
4

Na verdade, no primeiro caso, a expressão pode ser avaliada, pois o compilador sabe que deve ser avaliada como uma Integer, no entanto, no segundo caso, o tipo do valor de retorno ( null) não pode ser determinado, portanto não pode ser compilado. Se você o converter Integer, o código será compilado.

Obter
fonte
2
private int temp() {

    if (true) {
        Integer x = null;
        return x;// since that is fine because of unboxing then the returned value could be null
        //in other words I can say x could be null or new Integer(intValue) or a intValue
    }

    return (true ? null : 0);  //this will be prefectly legal null would be refrence to Integer. The concept is one the returned
    //value can be Integer 
    // then null is accepted to be a variable (-refrence variable-) of Integer
}
Youans
fonte
0

Que tal agora:

public class ConditionalExpressionType {

    public static void main(String[] args) {

        String s = "";
        s += (true ? 1 : "") instanceof Integer;
        System.out.println(s);

        String t = "";
        t += (!true ? 1 : "") instanceof String;
        System.out.println(t);

    }

}

A saída é verdadeira, verdadeira.

A cor do Eclipse codifica o 1 na expressão condicional como caixa automática.

Meu palpite é que o compilador está vendo o tipo de retorno da expressão como Object.

Jon
fonte