Por que comparar o Integer com o int pode gerar o NullPointerException em Java?

Foi muito confuso para mim observar esta situação:

Integer i = null; String str = null; if (i == null) { //Nothing happens ... } if (str == null) { //Nothing happens } if (i == 0) { //NullPointerException ... } if (str == "0") { //Nothing happens ... } 

Então, como eu acho que a operação de boxe é executada primeiro (ou seja, java tenta extrair o valor int de null ) e a operação de comparação tem menor prioridade é por isso que a exceção é lançada.

A questão é: por que ela é implementada dessa maneira em Java? Por que o boxe tem maior prioridade do que comparar referências? Ou por que eles não implementaram a verificação contra null antes do boxe?

No momento, ele parece inconsistente quando o NullPointerException é lançado com primitivos agrupados e não é lançado com tipos de objects verdadeiros .

A resposta curta

O ponto chave é este:

  • == entre dois tipos de referência é sempre comparação de referência
    • Mais frequentemente do que não, por exemplo, com Integer e String , você gostaria de usar equals vez disso
  • == entre um tipo de referência e um tipo primitivo numérico é sempre uma comparação numérica
    • O tipo de referência será sujeito a conversão de unboxing
    • Unboxing null sempre lança NullPointerException
  • Embora o Java tenha muitos tratamentos especiais para o String , ele não é, na verdade, um tipo primitivo

As declarações acima são válidas para qualquer código Java válido fornecido. Com esse entendimento, não há inconsistência no snippet que você apresentou.


A longa resposta

Aqui estão as seções relevantes do JLS:

JLS 15.21.3 Operadores de Igualdade de Referência == e !=

Se os operandos de um operador de igualdade forem do tipo de referência ou do tipo nulo , a operação será object de igualdade.

Isso explica o seguinte:

 Integer i = null; String str = null; if (i == null) { // Nothing happens } if (str == null) { // Nothing happens } if (str == "0") { // Nothing happens } 

Ambos os operandos são tipos de referência, e é por isso que o == é a comparação de igualdade de referência.

Isso também explica o seguinte:

 System.out.println(new Integer(0) == new Integer(0)); // "false" System.out.println("X" == "x".toUpperCase()); // "false" 

Para == ser igualdade numérica, pelo menos um dos operandos deve ser um tipo numérico :

JLS 15.21.1 Operadores de Igualdade Numérica == e !=

Se os operandos de um operador de igualdade forem ambos do tipo numérico, ou se um for do tipo numérico e o outro for convertível em tipo numérico, a promoção numérica binária será executada nos operandos. Se o tipo promovido dos operandos for int ou long , um teste de igualdade de inteiros será executado; se o tipo promovido for float or double`, será realizado um teste de igualdade de ponto flutuante.

Observe que a promoção numérica binária executa conversão de conjunto de valores e conversão de unboxing.

Isso explica:

 Integer i = null; if (i == 0) { //NullPointerException } 

Aqui está um trecho do Effective Java 2nd Edition, Item 49: Preferir primitivos para primitivos in a box :

Em resumo, use primitivos em vez de primitivos em checkbox sempre que tiver a escolha. Tipos primitivos são mais simples e rápidos. Se você deve usar primitivos em checkbox, tenha cuidado! Autoboxing reduz a verbosidade, mas não o perigo, de usar primitivos in a box. Quando seu programa compara duas primitivas em checkbox com o operador == , ele faz uma comparação de identidade, o que quase certamente não é o que você deseja. Quando o seu programa faz cálculos de tipo misto envolvendo primitivas in-box e unboxed, ele faz unboxing e, quando o seu programa faz unboxing, ele pode lançar NullPointerException . Finalmente, quando o seu programa contiver valores primitivos, isso pode resultar em criações de objects dispendiosas e desnecessárias.

Há lugares onde você não tem escolha a não ser usar primitivos em checkbox, por exemplo, genéricos, mas caso contrário, você deve considerar seriamente se a decisão de usar primitivas em checkbox é justificada.

Referências

  • JLS 4.2. Tipos primitivos e valores
    • “Os tipos numéricos são os tipos integrais e os tipos de ponto flutuante.”
  • JLS 5.1.8 Conversão de Unboxing
    • “Diz-se que um tipo é conversível em um tipo numérico se é um tipo numérico, ou é um tipo de referência que pode ser convertido em um tipo numérico por conversão de unboxing.”
    • “Conversão Unboxing […] converte do tipo Integer para tipo int
    • “Se r é null , a conversão de unboxing gera um NullPointerException
  • Guia de Linguagem Java / Autoboxing
  • JLS 15.21.1 Operadores de Igualdade Numérica == e !=
  • JLS 15.21.3 Operadores de Igualdade de Referência == e !=
  • JLS 5.6.2 Promoção numérica binária

Perguntas relacionadas

  • Ao comparar dois Integers no Java, ocorre o desempacotamento automático?
  • Por que estes são == mas não equals() ?
  • Java: Qual a diferença entre autoboxing e casting?

Perguntas relacionadas

  • Qual é a diferença entre um int e um Integer em Java / C #?
  • É garantido que o novo Integer (i) == i em Java? (Sim! A checkbox está fechada, não de outra maneira!)
  • Por que int num = Integer.getInteger("123") lança NullPointerException ? (!!!)
  • Java noob: generics over objects only? (sim Infelizmente)
  • Java String.equals versus ==

Seu exemplo NPE é equivalente a este código, graças ao autoboxing :

if ( i.intValue( ) == 0 )

Daí NPE se i é null .

 if (i == 0) { //NullPointerException ... } 

Eu é um inteiro e o 0 é um int assim no que realmente é feito é algo como isto

 i.intValue() == 0 

E isso causa o nullPointer porque o i é nulo. Para String, não temos essa operação, por isso não é uma exceção.

Os criadores de Java poderiam ter definido o operador == para atuar diretamente sobre operandos de tipos diferentes, em cujo caso dado Integer I; int i; Integer I; int i; a comparação I==i; poderia fazer a pergunta ” I uma referência a um Integer cujo valor é i ?” – uma pergunta que poderia ser respondida sem dificuldade, mesmo quando I é nulo. Infelizmente, o Java não verifica diretamente se os operandos de diferentes tipos são iguais; em vez disso, verifica se a linguagem permite que o tipo de operando seja convertido para o tipo da outra e, se isso acontecer, compara o operando convertido com o não convertido. Tal comportamento significa que, para as variables x , y com algumas combinações de tipos, é possível ter x==y e y==z mas x!=z [x = 16777216f y = 16777216 z = 16777217]. Isso também significa que a comparação I==i é traduzida como “Converta I em um int e, se isso não gerar uma exceção, compare-o a i “.

É por causa do recurso de autoboxing do Javas. O compilador detecta que, do lado direito da comparação, você está usando um inteiro primitivo e precisa unboxar o valor Integer do wrapper em um valor int primitivo também.

Como isso não é possível (é nulo quando você se NullPointerException ), o NullPointerException é lançado.

Em i == 0 Java tentará fazer o auto-unboxing e fazer uma comparação numérica (isto é, “é o valor armazenado no object wrapper referenciado por i igual ao valor 0 ?”).

Como i é null o unboxing lançará um NullPointerException .

O raciocínio é assim:

A primeira sentença do JLS § 15.21.1 Operadores de Igualdade Numérica == e! = É assim:

Se os operandos de um operador de igualdade forem ambos do tipo numérico, ou um for do tipo numérico e o outro for conversível (§5.1.8) para o tipo numérico, a promoção numérica binária será executada nos operandos (§5.6.2).

Claramente i é conversível para um tipo numérico e 0 é um tipo numérico, portanto, a promoção numérica binária é executada nos operandos.

§ 5.6.2 A promoção numérica binária diz (entre outras coisas):

Se algum dos operandos for de um tipo de referência, a conversão de unboxing (§5.1.8) é executada.

§ 5.1.8 Conversão de Unboxing diz (entre outras coisas):

Se r for nulo, a conversão de unboxing lança um NullPointerException