Layer 8Ehrlicher Tech-Support

Java: Integer mit == vergleichen, bis 127 gut, ab 128 Glück

Zwei Integer mit dem Wert 100 sind per == gleich, bei 1000 ist das offen. Die JLS garantiert identische Objekte nur von -128 bis 127.

Aufwand:
ca. 20 Min.
Voraussetzung:
Ein JDK, hier geprüft mit OpenJDK 25, Der java-Starter im Quelldatei-Modus für den Selbsttest, Code-Review mit Blick auf == zwischen Objekten
Gilt für:
Java SE 25 (JLS §5.1.7, §15.21 und API-Dokumentation Java SE 25; der Wortlaut von §5.1.7 ist in der JLS für Java SE 21 identisch)

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

Häufige Fragen (FAQ)

?Warum ist 100 == 100 bei Integer-Objekten true, 1000 == 1000 aber false?

Weil == bei zwei Objekten nicht die Werte vergleicht, sondern prüft, ob beide auf dasselbe Objekt zeigen; so steht es in JLS §15.21.3. Für kleine Werte sorgt ein Cache dafür, dass es dasselbe Objekt ist: Die JLS verlangt das in §5.1.7 für geboxte Konstantenausdrücke im Bereich -128 bis 127, und Integer.valueOf sagt in seiner Dokumentation zu, Werte in diesem Bereich immer zu cachen. Für 1000 gibt es keine solche Zusage. Dass dort false herauskommt, ist erlaubt, aber ebenfalls nicht garantiert.

?Kommt ab 128 also garantiert false heraus?

Nein, und genau das macht den Fehler so tückisch. Die JLS verbietet für Werte außerhalb des Bereichs lediglich jede Annahme über die Identität: „For other values, the rule disallows any assumptions about the identity of the boxed values on the programmer's part.“ Sie erlaubt ausdrücklich, aber verlangt nicht, dass auch diese Referenzen geteilt werden, und nennt als Beispiel Implementierungen, die int-Werte im Bereich von -32K bis +32K cachen könnten. Integer.valueOf darf laut API ebenfalls weitere Werte cachen. Ein Test, der bei 1000 false sieht, beweist also nur etwas über diese eine JVM.

?Was ist der richtige Vergleich?

equals oder Objects.equals. Integer.equals liefert laut API genau dann true, wenn das Argument nicht null ist, ein Integer ist und denselben int-Wert enthält. Objects.equals ist zusätzlich null-sicher: Sind beide Argumente null, ist das Ergebnis true, sonst entscheidet equals des ersten Arguments. Wer ohnehin mit Zahlen rechnet, kann auch die int-Werte mit intValue vergleichen, muss dann aber vorher sicherstellen, dass keines der Objekte null ist.

?Und wenn ich einen Integer mit einem int per == vergleiche?

Dann wird tatsächlich der Wert verglichen. Ist ein Operand von numerischem Typ und der andere in einen numerischen Typ umwandelbar, führt Java laut JLS §15.21.1 eine numerische Typangleichung durch, die ein Unboxing einschließen kann, und vergleicht dann Zahlen. Der Haken liegt woanders: Ist der Integer null, wirft das Unboxing laut JLS §5.1.8 eine NullPointerException. Der Vergleich mit einem int ist also korrekt, aber nicht null-sicher.

?Ist das nicht einfach ein Implementierungsdetail, das man kennen kann?

Man kann es kennen, sollte sich aber nicht darauf verlassen. Die API-Dokumentation führt Integer als wertbasierte Klasse, und für solche Klassen schreibt Oracle, ein Programm solle bei gleichen Instanzen nicht versuchen, ihre Identität zu unterscheiden, weder direkt über Referenzgleichheit noch indirekt über Synchronisation, Identitäts-Hashcodes oder Serialisierung. Dazu kommt: „Identity-related behavior of value-based classes may change in a future release.“ Wer heute auf == setzt, setzt auf etwas, das ausdrücklich nicht zugesagt ist.

Belege

Diese Seite ist gegen die folgenden Primärquellen geprüft – Hersteller, Standard oder Gesetzestext. Wer es nicht glaubt, muss es nicht: Da steht, wo es nachzulesen ist.

  1. [4]
    Java SE 25 & JDK 25 API Specification, Value-based Classes

    docs.oracle.com · abgerufen am

Zuletzt geprüft am . Dokumentation altert – ein Datum daneben ist der einzige ehrliche Umgang damit.

Auch zu finden unter

Verwandte Fehler