Dlaczego porównanie liczby całkowitej z wartością int może zgłosić wyjątek NullPointerException w języku Java?

81

Obserwowanie tej sytuacji było dla mnie bardzo zagmatwane:

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

Tak więc, jak myślę, operacja pakowania jest wykonywana jako pierwsza (tj. Java próbuje wyodrębnić wartość int null), a operacja porównania ma niższy priorytet, dlatego zgłaszany jest wyjątek.

Pytanie brzmi: dlaczego jest tak zaimplementowany w Javie? Dlaczego boks ma wyższy priorytet niż porównywanie odniesień? Albo dlaczego nie wdrożyli weryfikacji nullprzed boksowaniem?

W tej chwili wygląda to niespójnie, gdy NullPointerExceptionjest wyrzucane z opakowanymi prymitywami i nie jest wyrzucane z prawdziwymi typami obiektów.

rzymski
źródło
Otrzymałbyś NullPointerException, gdybyś zrobił str.equals ("0").
Ash Burlaczenko
Kiedyś operator == był chroniony przed NPE w każdych okolicznościach. Dla mnie to tylko kolejny przykład, który pokazuje, jak złym pomysłem było wprowadzenie auto-boksu w Javie. Po prostu nie pasuje z tak wielu powodów i nie oferuje niczego, czego wcześniej nie było. To tylko sprawia, że ​​kod jest krótszy, podczas gdy przesłania to, co naprawdę się dzieje.
x4u
Moje myśli różnią się o 180 stopni. Nie powinni byli uwzględniać prymitywów używanych wszędzie. Następnie pozwól kompilatorowi zoptymalizować i użyć prymitywów. Wtedy nie byłoby zamieszania.
MrJacqes

Odpowiedzi:

138

Krótka odpowiedź

Kluczowa kwestia jest taka:

  • == między dwoma typami odwołań jest zawsze porównaniem referencyjnym
    • Najczęściej, np. Z Integeri String, equalszamiast tego chciałbyś użyć
  • == między typem referencyjnym a liczbowym typem pierwotnym jest zawsze porównanie liczbowe
    • Typ referencyjny zostanie poddany konwersji podczas rozpakowywania
    • Unboxing nullzawsze rzucaNullPointerException
  • Chociaż Java ma wiele specjalnych metod leczenia String, w rzeczywistości NIE jest to typ prymitywny

Powyższe instrukcje dotyczą dowolnego poprawnego kodu Java. Z tym zrozumieniem nie ma żadnych niespójności w przedstawionym fragmencie.


Długa odpowiedź

Oto odpowiednie sekcje JLS:

JLS 15.21.3 Operatory równości odwołań ==i!=

Jeśli operandy operatora równości są typu referencyjnego lub typu null , operacja jest równa obiektowi.

To wyjaśnia następujące kwestie:

Integer i = null;
String str = null;

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

Oba operandy są typami referencyjnymi i dlatego ==jest porównaniem równości odwołań.

To wyjaśnia również następujące kwestie:

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

Aby ==być równością liczbową, co najmniej jeden z argumentów musi być typu liczbowego :

JLS 15.21.1 Liczbowe operatory równości ==i!=

Jeśli oba operandy operatora równości są typu liczbowego lub jeden jest typu liczbowego, a drugi można konwertować na typ liczbowy, na operandach jest wykonywana binarna promocja liczbowa. Jeśli promowanym typem operandów jest intlub long, wykonywany jest test równości liczb całkowitych; jeśli promowany typ to float or double`, to wykonywany jest zmiennoprzecinkowy test równości.

Zwróć uwagę, że binarna promocja liczbowa wykonuje konwersję zestawu wartości i konwersję po rozpakowaniu.

To wyjaśnia:

Integer i = null;

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

Oto fragment z Effective Java 2nd Edition, pozycja 49: Preferuj prymitywy od pudełkowych :

Podsumowując, zawsze używaj prymitywów zamiast prymitywów pudełkowych, kiedy tylko masz wybór. Typy prymitywne są prostsze i szybsze. Jeśli musisz używać prymitywów w pudełkach, bądź ostrożny! Autoboxing redukuje gadatliwość, ale nie ogranicza niebezpieczeństwa używania pudełkowych prymitywów. Kiedy twój program porównuje dwa prymitywy w pudełku z ==operatorem, dokonuje porównania tożsamości, co prawie na pewno nie jest tym, czego chcesz. Kiedy twój program wykonuje obliczenia typu mieszanego z udziałem prymitywów zapakowanych i nieopakowanych, robi to rozpakowywanie, a kiedy program wykonuje rozpakowywanie, może rzucać NullPointerException. Wreszcie, gdy program zawiera prymitywne wartości, może to spowodować kosztowne i niepotrzebne tworzenie obiektów.

Są miejsca, w których nie masz innego wyboru, jak tylko użyć prymitywów pudełkowych, np. Generycznych, ale w przeciwnym razie powinieneś poważnie rozważyć, czy decyzja o użyciu prymitywów pudełkowych jest uzasadniona.

Bibliografia

Powiązane pytania

Powiązane pytania

smary wielogenowe
źródło
2
Jeśli chodzi o to, dlaczego someRef == 0 zawsze jest to porównanie numeryczne, jest to bardzo rozsądny wybór, ponieważ porównywanie odwołań do dwóch prymitywów pudełkowych jest prawie zawsze błędem programisty. Domyślne porównywanie odniesień w tym przypadku byłoby bezcelowe.
Mark Peters
3
Dlaczego nie kompilator zastąpić wyraz (myInteger == 0)z (myInteger != null && myInteger == 0)zamiast opierania się na autora do napisania tej szablonowe kod null sprawdzania? IMO Powinienem być w stanie sprawdzić, if (myBoolean)a to powinno ocenić truewtedy i tylko wtedy, gdy podstawowa wartość jest konkretna true- nie powinienem najpierw musieć sprawdzać wartości zerowej.
Josh M.
15

Twój przykład NPE jest odpowiednikiem tego kodu dzięki autoboxingowi :

if ( i.intValue( ) == 0 )

Stąd NPE, jeśli ijest null.

Alexander Pogrebnyak
źródło
4
if (i == 0) {  //NullPointerException
   ...
}

i jest liczbą całkowitą, a 0 jest liczbą całkowitą, więc w tym, co naprawdę się robi, wygląda to tak

i.intValue() == 0

A to powoduje nullPointer, ponieważ i jest null. Dla String nie mamy tej operacji, dlatego nie ma tam wyjątku.

Damian Leszczyński - Vash
źródło
4

Twórcy Javy mogli zdefiniować ==operator tak, aby działał bezpośrednio na operandach różnych typów. W takim przypadku, biorąc Integer I; int i;pod uwagę porównanie, I==i;można zadać pytanie „Czy Izawiera odniesienie do elementu, Integerktórego wartość jest i?” - pytanie, na które można odpowiedzieć bez trudności nawet jeśli Ijest zerowa. Niestety, Java nie sprawdza bezpośrednio, czy operandy różnych typów są równe; zamiast tego sprawdza, czy język zezwala na konwersję jednego z typów operandów na typ drugiego i - jeśli tak - porównuje przekonwertowany operand z nieprzekonwertowanym. Takie zachowanie oznacza, że dla zmiennych x, yoraz zz niektóre kombinacje typów, to jest możliwe, że x==yi y==zalex!=z[np. x = 16777216f y = 16777216 z = 16777217]. Oznacza to również, że porównanie I==ijest tłumaczone jako „Konwertuj I na an, inta jeśli to nie zgłasza wyjątku, porównaj je z i”.

supercat
źródło
+1: Za to, że faktycznie próbujesz odpowiedzieć na pytanie OP: „Dlaczego to jest tak zaprojektowane?”
Martijn Courteaux
1
@MartijnCourteaux: Wiele języków wydaje się definiować operatory tylko dla operandów pasujących typów i zakłada, że ​​jeśli T jest kiedykolwiek niejawnie konwertowane na U, taka niejawna konwersja powinna być wykonywana bez zarzutu za każdym razem, gdy U może zostać zaakceptowane, a T nie. Gdyby nie takiego zachowania, język mógłby określić ==w taki sposób, że jeśli we wszystkich przypadkach x==y, y==zi x==zwszystkich kompilacji bez zarzutu, trzy porównania będzie zachowywać się jak relacją równoważności. Ciekawe, że projektanci naciskają na różne wymyślne funkcje językowe, ale ignorują aksjomatyczną zgodność.
supercat
1

Dzieje się tak z powodu funkcji automatycznego blokowania Javas . Kompilator wykrywa, że ​​po prawej stronie porównania używasz prymitywnej liczby całkowitej i musi również rozpakować opakowującą wartość Integer na pierwotną wartość int.

Ponieważ nie jest to możliwe (jest zerowe, jak wskazałeś), NullPointerExceptionzostaje wyrzucony.

perdian
źródło
1

W i == 0Javie spróbuje wykonać automatyczne rozpakowywanie i wykonać porównanie liczbowe (tj. "Czy wartość przechowywana w obiekcie opakowującym odwołuje się do itej samej wartości 0?").

Ponieważ ijest nullto rozpakowanie, wyrzuci plik NullPointerException.

Rozumowanie wygląda następująco:

Pierwsze zdanie JLS § 15.21.1 Numerical Equality Operators == i! = Brzmi następująco:

Jeśli oba operandy operatora równości są typu liczbowego lub jeden jest typu liczbowego, a drugi można zamienić (§5.1.8) na typ liczbowy, binarna promocja liczbowa jest wykonywana na operandach (§5.6.2).

Oczywiście imożna konwertować na typ liczbowy i 0jest typem liczbowym, więc binarna promocja liczbowa jest wykonywana na operandach.

§ 5.6.2 Binarna promocja numeryczna mówi (między innymi):

Jeśli którykolwiek z operandów jest typu referencyjnego, wykonywana jest konwersja po rozpakowaniu (§5.1.8).

§ 5.1.8 Unboxing Conversion mówi (między innymi):

Jeśli r jest null, konwersja podczas rozpakowywania generuje plikNullPointerException

Joachim Sauer
źródło
0

Po prostu napisz metodę i wywołaj ją, aby uniknąć wyjątku NullPointerException.

public static Integer getNotNullIntValue(Integer value)
{
    if(value!=null)
    {
        return value;
    }
    return 0;
}
Satish Hawalppagol
źródło