Das Symptom
Der Entwickler rechnet Summen, Millisekunden, Bytezahlen oder Centbeträge in int und verlässt sich darauf, dass Java bei einem zu großen Ergebnis abbricht oder wenigstens warnt. Tut es nicht: Ab 2147483647 kippt das Ergebnis lautlos ins Negative, eine Dateigröße wird negativ, ein Zeitunterschied springt zurück, eine Summe wird kleiner statt größer. Der Fehler fällt nicht beim Rechnen auf, sondern erst irgendwo stromabwärts, wo ein negativer Wert keinen Sinn ergibt, und der Verdacht fällt auf die Eingabedaten.
Die „Layer 8“-Wahrheit
Die Java-Sprachspezifikation legt in Abschnitt 4.2.2 fest: „The integer operators do not indicate overflow or underflow in any way.“ Das ist kein Versäumnis, sondern eine Entscheidung, und sie gilt für jede Addition, Subtraktion und Multiplikation auf int und long. Abschnitt 15.18.2 beschreibt, was stattdessen passiert: „If an integer addition overflows, then the result is the low-order bits of the mathematical sum as represented in some sufficiently large two’s-complement format. If overflow occurs, then the sign of the result is not the same as the sign of the mathematical sum of the two operand values.“ Der Wertebereich von int ist nach Abschnitt 4.2.1 „from -2147483648 to 2147483647, inclusive“; was darüber hinausgeht, beginnt am anderen Ende wieder.
Eine Ausnahme wirft Java bei Ganzzahlen nur in einem Fall, der Division durch null. Alles andere ist ein definiertes Ergebnis, und ein definiertes Ergebnis löst keinen Alarm aus. Wer eine Fehlermeldung will, muss sie bestellen: die Exact-Methoden der Klasse Math werfen bei Überlauf eine ArithmeticException. Die API-Dokumentation beschreibt addExact als Methode, die „Returns the sum of its arguments, throwing an exception if the result overflows an int.“ und nennt als Ausnahme „ArithmeticException
- if the result overflows an int“; vorhanden ist sie seit Java 1.8. Für Beträge und Zähler, die groß werden können, ist long die erste Wahl, für Geld BigDecimal, und bei Produkten muss der erste Operand bereits long sein, sonst rechnet Java in int und wandelt erst das falsche Ergebnis um.
Die Erwartung, dass eine sichere Sprache auch vor großen Zahlen schützt, ist nachvollziehbar und falsch. Java sichert Speicher und Typen; die Ganzzahlen folgen dem Zweierkomplement, und das Zweierkomplement kennt keinen Fehler, nur einen Rand. Wer an den Rand gerät, merkt es nicht an der Rechnung, sondern an dem Ort, an dem ein Minus steht, wo keines sein kann.
Dass andere Sprachen es anders halten, macht die Sache nicht einfacher. Wer von einer Umgebung kommt, die bei Überlauf abbricht oder automatisch auf beliebig große Zahlen wechselt, bringt die Erwartung mit, dass eine Rechnung entweder stimmt oder scheitert. In Java gibt es eine dritte Möglichkeit: Sie stimmt modulo zwei hoch zweiunddreißig, und das sieht von außen aus wie Scheitern mit Verzögerung. Die Spezifikation versteckt das nicht; sie sagt es in einem Satz, der nur selten gelesen wird, weil niemand die Spezifikation aufschlägt, um zu erfahren, was plus bedeutet.