Das Symptom
Zwei Integer-Objekte werden mit == verglichen, etwa eine ID aus einer Liste mit der ID
aus einer Map. Alle Tests sind grün, denn die Testdaten haben IDs wie 1, 2 und 42. Im
Betrieb sind die IDs vierstellig, und plötzlich sind 1000 und 1000 nicht mehr gleich.
Der Debugger zeigt auf beiden Seiten denselben Wert, der Vergleich liefert trotzdem
false.
Weil sich das Verhalten mit kleinen Zahlen nicht nachstellen lässt, beginnt die Jagd an
der falschen Stelle: Threading, die Datenbank, der Treiber, die JVM-Version. Dabei war
der Code vom ersten Tag an falsch und hatte nur bis 127 Glück.
Die „Layer 8“-Wahrheit
== vergleicht bei zwei Objekten keine Werte, sondern Referenzen. JLS §15.21.3 sagt es
ohne Spielraum: „At run time, the result of == is true if the operand values are both
null or both refer to the same object or array; otherwise, the result is false.“ Die
Frage ist also nicht, ob 1000 gleich 1000 ist, sondern ob beide Seiten zufällig auf
dasselbe Objekt zeigen.
Bei kleinen Zahlen tun sie das, und zwar absichtlich. Die JLS legt in §5.1.7 fest: Wird
ein Konstantenausdruck geboxt, dessen Ergebnis eine ganze Zahl im Bereich -128 bis 127
ist, dann gilt für zwei solche Boxings: „It is always the case that a == b.“ Sie nennt
das selbst einen Kompromiss: „The rule above is a pragmatic compromise, requiring that
certain common values always be boxed into indistinguishable objects.“ Für Werte, die
erst zur Laufzeit entstehen, liefert Integer.valueOf die passende Zusage: „This method
will always cache values in the range -128 to 127, inclusive, and may cache other
values outside of this range.“
Damit ist auch die zweite Hälfte der Wahrheit gesagt, die leicht untergeht: Außerhalb des
Bereichs ist false nicht garantiert, sondern nur erlaubt. Die JLS verbietet dort jede
Annahme über die Identität, erlaubt einer Implementierung aber ausdrücklich, mehr zu
cachen, und nennt als Beispiel sogar den Bereich von -32K bis +32K. Wer also mit einem
Test nachweist, dass 1000 == 1000 false ergibt, hat nur etwas über seine JVM gelernt,
nicht über Java. Auf der für diesen Eintrag verwendeten OpenJDK 25 war es false.
Der Fehler ist deshalb kein Heisenbug, sondern ein Lesefehler im eigenen Code. Integer
ist laut API eine wertbasierte Klasse, und Oracle schreibt für solche Klassen, dass ein
Programm bei gleichen Instanzen nicht versuchen soll, ihre Identität zu unterscheiden,
auch nicht über Referenzgleichheit. Der richtige Vergleich heißt equals oder, wenn null
vorkommen kann, Objects.equals. Und der Test, der das früher gezeigt hätte, braucht nur
eine ID größer als 127.
Die echte, verifizierte Lösung
1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:
1.Die Stellen finden, an denen Objekte mit == verglichen werden
Gesucht sind Vergleiche mit == oder != zwischen zwei Ausdrücken vom Typ Integer, etwa IDs aus Listen, Werte aus Maps oder Felder von Datenobjekten. Dasselbe Muster betrifft auch Long, Short und die anderen Hüllklassen. Vergleiche zwischen einem Integer und einem int sind nicht das Problem, weil dort laut JLS §15.21.1 die Werte verglichen werden; sie sind aber nicht null-sicher.
2.Den Effekt selbst nachprüfen
Das kleine Programm erzeugt aus einer Zahl auf der Kommandozeile zwei Integer-Objekte über Integer.valueOf und vergleicht sie dreimal. Es lässt sich ohne separaten Aufruf von javac mit dem java-Starter im Quelldatei-Modus starten, der die Datei selbst übersetzt und ausführt, also mit java IntegerVergleich.java gefolgt von der Zahl. Mit 100 und mit -128 lieferte es bei der Prüfung mit OpenJDK 25 dreimal true, mit 1000 und mit -129 beim ersten Vergleich false.
import java.util.Objects;
public class IntegerVergleich {
public static void main(String[] args) {
int wert = Integer.parseInt(args[0]);
Integer a = Integer.valueOf(wert);
Integer b = Integer.valueOf(wert);
System.out.println("a == b: " + (a == b));
System.out.println("a.equals(b): " + a.equals(b));
System.out.println("Objects.equals: " + Objects.equals(a, b));
}
}
3.Auf equals oder Objects.equals umstellen
Jeder Wertvergleich zwischen zwei Integer-Objekten wird zu a.equals(b), wenn a sicher nicht null ist, oder zu Objects.equals(a, b), wenn null vorkommen kann. Objects.equals gibt laut API bei zwei null-Werten true zurück und ruft sonst equals des ersten Arguments auf. Damit ist der Vergleich unabhängig davon, ob die JVM ein Objekt aus dem Cache liefert oder ein neues erzeugt.
4.Wo möglich primitive int verwenden
Wo ein Wert nie null sein muss und nicht in einer generischen Sammlung steckt, ist der primitive Typ int die ehrlichere Wahl. Zwischen zwei int vergleicht == immer die Zahl, und eine NullPointerException durch Unboxing kann gar nicht erst entstehen. Hüllklassen gehören dorthin, wo Java sie verlangt, also in Sammlungen und Generics, oder wo null eine echte Bedeutung hat.
5.Testdaten jenseits von 127 wählen
Der Fehler überlebt Tests, wenn die Testdaten klein sind: IDs 1, 2, 3 liegen alle im Bereich, den die JLS und Integer.valueOf garantiert cachen. Wer IDs und Zähler in Tests bewusst über 127 und unter -128 wählt, macht verborgene Referenzvergleiche sichtbar, zumindest auf einer JVM, die diese Werte nicht zusätzlich cacht. Ein grüner Test mit kleinen Zahlen beweist hier nichts.