String in int umwandeln in Java - Feature-Bild

Java: String in int umwandeln – Besonderheiten und Fallstricke

Im vorherigen Artikel habe ich euch gezeigt, dass "" + i die schnellste Methode ist, um in Java ein int in einen String umzuwandeln. Und zwar durchgehend von Java 7 bis Java 14.

Heute erfahrt ihr, was bei der entgegengesetzten Richtung, also beim Parsen von Strings in ints zu beachten ist. Den Quellcode zum Artikel findet ihr in meinem GitLab-Repository.

Zahlen im Dezimalsystem parsen

Schauen wir uns zunächst einmal die Möglichkeiten an, ein String in ein int (bzw. Integer) zu parsen. Bis Java 7 gibt es folgende Varianten:

  • int i = Integer.parseInt(s); (→ JavaDoc)
  • Integer i = Integer.valueOf(s); (→ JavaDoc)

Die zweite Methode ruft intern die erste Methode auf und verpackt das Ergebnis in ein Integer-Objekt.

Der übergebene String darf nur Ziffern enthalten, optional mit einem vorangestellten Plus oder Minus-Zeichen. Folgendes ist also erlaubt:

  • Integer.parseInt("47")
  • Integer.parseInt("+86400")
  • Integer.parseInt("-1")

Folgende Strings hingehen dürfen nicht übergeben werden und führen zu NumberFormatExceptions:

  • Integer.parseInt("") // Leerer String nicht erlaubt
  • Integer.parseInt(" 1") // Leerzeichen nicht erlaubt
  • Integer.parseInt("3,14") // Komma nicht erlaubt
  • Integer.parseInt("1.000") // Tausender-Punkt nicht erlaubt

Hexadezimal- und Binärzahlen parsen

Die zuvor genannten Methoden parsen Dezimalzahlen. Um andere Zahlensysteme zu parsen, stehen folgende überladene Methode zur Verfügung:

  • int i = Integer.parseInt(s, radix); (→ JavaDoc)
  • Integer i = Integer.valueOf(s, radix); (→ JavaDoc)

Der Parameter radix gibt dabei die Basis des Zahlensystems an. Eine hexadezimale Zahl parst man bspw. wie folgt:

  • Integer.parseInt("CAFE", 16)

Und eine Binärzahl so:

  • Integer.parseInt("101111", 2)

Signed vs. unsigned ints

In allen vorgenannten Fällen muss die zu parsende Zahl im Bereich Integer.MIN_VALUE (= -231 = -2.147.483.648) bis Integer.MAX_VALUE (= 231-1 = 2.147.483.647) liegen.

Interessant (um nicht zu sagen: verwirrend) wird es, wenn man z. B. den gültigen int-Wert 0xCAFEBABE in einen Hex-String umwandelt und dann zurück in ein int:

int hex = 0xCAFEBABE;
String s = Integer.toHexString(hex);
int i = Integer.parseInt(s, 16);

Dieser Versuch führt zu folgendem Fehler:

Exception in thread "main" java.lang.NumberFormatException: For input string: "cafebabe"

Warum ist das so?

Zunächst einmal: Der String s enthält wie erwartet „cafebabe“. Warum kann dieser String nicht zurück in ein int umgewandelt werden?

Der Grund ist, dass die parseInt()-Methode davon ausgeht, dass die übergebene Zahl positiv ist, sofern ihr nicht ein Minuszeichen vorangestellt ist. Konvertiert man „cafebabe“ ins Dezimalsystem, erhält man 3.405.691.582. Diese Zahl ist größer als Integer.MAX_INT und kann somit nicht als int dargestellt werden kann. 

Warum aber können wir die Zahl dann der int-Variablen hex zuweisen? Hier spielt uns die Binärdarstellung der Zahlen einen (beabsichtigten) Streich. 0xCAFEBABE entspricht binär 11001010.11111110.10111010.10111110 – einer 32-stelligen Binärzahl, deren erstes Bit eine 1 ist. Bei einem – in Java immer vorzeichenbehafteten – int steht das erste Bit für das Vorzeichen. Steht dort eine 1, ist die Zahl negativ (für Details zur Darstellung negativer Zahlen verweise ich auf diesen Wikipedia-Artikel). Erweitern wir den Code oben wie folgt um Debug-Ausgaben:

int hex = 0xCAFEBABE;
System.out.println("hex        = " + hex);
System.out.println("hex binary = " + Integer.toBinaryString(hex));

String s = Integer.toHexString(hex);
System.out.println("s          = " + s);

int i = Integer.parseInt(s, 16);
System.out.println("i          = " + i);

Dann sehen wir, dass hex den negativen Wert -889.275.714 enthält (die Tausenderpunkte habe hier der Übersicht halber eingefügt). Hexadezimal wird diese negative Zahl als positiver Wert „cafebabe“ dargestellt, welcher wiederum durch die parseInt()-Methode nicht zurückgewandelt werden kann.

Um dies doch noch zu ermöglichen, wurden in Java 8 folgende Methoden hinzugefügt:

  • int i = Integer.parseUnsignedInt(s); (→ JavaDoc)
  • int i = Integer.parseUnsignedInt(s, radix); (→ JavaDoc)

Diese Methoden erlauben uns Zahlen im Bereich 0 bis 4.294.967.295 (= 0xffffffff hexadezimal bzw. 32 Einsen im Binärsystem) zu parsen. Ab Java 8 können wir somit die vorletzte Zeile des obigen Beispiels wie folgt anpassen:

int i = Integer.parseUnsignedInt(s, 16);

Als Ausgabe erhalten wir aber nicht etwa 3.405.691.582 – sondern, da der Java-int nun eben immer signed (vorzeichenbehaftet) ist: -889.275.714, also denjenigen int-Wert, den wir auch erhalten, wenn wir 0xCAFEBABE einem int zuweisen.

Und wie kommen wir an die 3.405.691.582? Dazu müssen wir „cafebabe“ (oder „CAFEBABE“ – die Groß-/Kleinschreibung ist unbedeutend) in ein long parsen:

long l = Long.parseLong(s, 16);

Und wie sieht 3.405.691.582 in Binär- und Hexadezimaldarstellung aus?

System.out.println("l binary = " + Long.toBinaryString(l));
System.out.println("l hex    = " + Long.toHexString(l));

Wir erhalten wiederum die gleichen Darstellungen wie für den int-Wert -889.275.714, also 11001010.11111110.10111010.10111110 und „cafebabe“. EIn- und dieselbe Binär- bzw. Hexadezimalzahl führt also – je nachdem, ob sie in einem int oder einem long gespeichert wird – zu einer unterschiedlichen Dezimalzahl (sofern sie größer ist als Integer.MAX_VALUE). Im folgenden Abschnitt schauen wir uns das noch einmal an weiteren Beispielen an.

parseInt() vs. parseUnsignedInt()

Um den Unterschied zwischen parseInt() und parseUnsignedInt() noch einmal zu verdeutlichen, habe ich ein kleines Programm geschrieben, das ihr hier in meinem GitLab-Repository findet, und das verschiedene (Grenz-)Werte mit beiden Methoden parst.

Hier findet ihr das Ergebnis zusammengefasst (die Striche stehen für NumberFormatExceptions):

StringBemerkungparseInt()Hexparse
Unsigned
Int()
Hex
-2147483649Integer.MIN_VALUE – 1
-2147483648Integer. MIN_VALUE-214748364880000000
-1000000000-1000000000c4653600
-1-1ffffffff
00000
100000000010000000003b9aca0010000000003b9aca00
2147483647Integer.MAX_VALUE21474836477fffffff21474836477fffffff
2147483648Integer.MAX_VALUE +1-214748364880000000
3000000000-1294967296b2d05e00
42949672952 * Integer.MAX_VALUE + 1-1ffffffff
42949672962 * Integer.MAX_VALUE + 2

Hier ist noch einmal schön zu sehen:

  • im Bereich 0 bis Integer.MAX_VALUE liefern parseInt() und parseUnsignedInt() die gleichen Ergebnisse.
  • parseInt() deckt darüberhinaus den Bereich bis Integer.MIN_VALUE ab und liefert exakt den Wert, der übergeben wurde.
  • parseUnsignedInt() deckt hingegen den Bereich bis 2 * Integer.MAX_VALUE + 1 mit ab – das Ergebnis ist jedoch im Bereich über Integer.MAX_VALUE immer eine negative Zahl. Deren hexadezimale Darstellung entspricht, umgerechnet ins Dezimalsystem, wiederum dem Eingabewert.

Auto-Boxing und -Unboxing des Ergebnisses

Wir haben oben gesehen, dass es separate Methoden gibt, um Strings in ein int bzw. ein Integer-Objekt zu konvertieren. Doch was passiert, wenn wir die falsche Methode anwenden?

  1. Integer i = Integer.parseInt("42");
  2. int i = Integer.valueOf("555");

Der erste Fall ist nicht besonders elegant, stellt aber kein Problem dar: Integer.parseInt() arbeitet intern mit primitiven Werten und das Ergebnis wird – genau wie bei Integer.valueOf() – letztlich durch Auto-Boxing in ein Integer-Objekt umgewandelt.

Anders sieht es im zweiten Fall aus: hier wird das Ergebnis innerhalb von Integer.valueOf() in ein Integer-Objekt umgewandelt und dann bei der Zuweisung an i wieder zurück in ein int-Primitiv. IntelliJ erkennt das (Eclipse hingegen nicht) und zeigt eine entsprechende Warnung an mit der Empfehlung valueOf() durch parseInt() zu ersetzen:

Screenshot der IntelliJ-Warnung bzgl. redundantem Boxing
IntelliJ warnt vor redundantem Boxing

Inwieweit der Compiler bzw. Hotspot uns diesen Fehler verzeiht, werden wir im nächsten Kapitel, „Performance“, überprüfen.

Performance der String-int-Umwandlung

Ich habe – ähnlich wie im letzten Artikel – mit Hilfe des Java Microbenchmark Harness – kurz: JMH – folgende Vergleichsmessungen durchgeführt:

  • Geschwindigkeit verschiedener String-zu-int-Umwandlungsmethoden mit Java 8:
    • parseInt() mit positiven Zahlen, positiven Zahlen mit vorangestelltem Plus-Zeichen und negativen Zahlen,
    • parseUnsignedInt() mit positiven Zahlen und positiven Zahlen mit vorangestelltem Plus-Zeichen,
    • valueOf() mit positiven Zahlen,
    • parseInt() mit anschließender Konvertierung in ein Integer-Objekt,
    • valueOf() mit anschließender Konvertierung in ein int-Primitiv,
  • Vergleich der parseInt()-Methode über alle Java-Versionen von Java 7 bis Java 14.

Performance der verschiedenen String-zu-int-Umwandlungsmethoden

Den Test findet ihr in meinem GitLab-Repository. Die Testergebnisse liegen im results/-Verzeichnis. Hier das Ergebnis des Vergleichs der verschiedenen Methodenaufrufe unter Java 8:

MethodeOperationen pro SekundeKonfidenz​intervall (99,9 %)
parseInt() positiver Wert25.157.28924.959.166 – 25.355.412
parseInt() positiver Wert mit Plus25.056.42724.974.885 – 25.137.970
parseInt() negativer Wert25.143.74025.039.972 – 25.247.508
parseUnsignedInt() positiver Wert25.124.02725.060.833 – 25.187.221
parseUnsignedInt() positiver Wert mit Plus25.015.08224.914.320 – 25.115.843
parseInt() mit anschließendem Boxing24.594.33624.421.316 – 24.767.355
valueOf() positiver Wert24.531.18724.413.040 – 24.649.334
valueOf() mit anschließendem Unboxing24.325.34724.183.155 – 24.467.538
Performance der String-zu-int-Umwandlungsmethoden unter Java 8
Performance der String-zu-int-Umwandlungsmethoden unter Java 8

Ihr seht, dass die ersten fünf Messergebnisse nahezu identisch sind. Das ist schnell erklärt: der aufgerufene Code ist in allen Fällen der gleiche. valueOf() und parseInt() mit anschließendem Boxing sind knapp 2 % langsamer. Dies dürfte dem Overhead für das Umwandeln in ein Integer-Objekt entsprechen. valueOf() mit anschließendem Unboxing ist noch einmal knapp 1 % langsamer, was bedeutet, dass weder der Compiler noch Hotspot uns den Fehler „Boxing mit anschließendem Unboxing“ verziehen haben.

Das Parsen negativer Zahlen sollte minimal schneller sein, da intern negative Zahlen aufaddiert werden und im Fall einer positiven Zahl das Ergebnis mit -1 multipliziert wird. In den Benchmarks lassen sich jedoch keine Unterschiede feststellen. Multiplizieren mit -1 ist offenbar so schnell, dass dies selbst bei 25 Millionen Multiplikationen pro Sekunde nicht ins Gewicht fällt.

Performance der String-zu-int-Konvertierung im Laufe der Java-Versionen

Da im Endeffekt alle Varianten der String-zu-int-Konvertierung Integer.parseInt() aufrufen, habe ich mich darauf beschränkt, die Performance des Aufrufs dieser einen Methode über die verschiedenen Java-Versionen hinweg zu messen. Ich habe hierfür dieselbe Test-Klasse verwendet wie für den vorherigen Test und alle Methoden außer integerParsePositiveInt() auskommentiert. Den Code habe ich mit den jeweiligen Java-Versionen compiliert und ausgeführt. Auch die Ergebnisse dieser Tests findet ihr im results/-Ordner. Hier eine Zusammenfassung der Ergebnisse:

Java-Version Operationen pro Sekunde Konfidenz​intervall (99,9 %)
Java 725.223.11725.069.748 – 25.376.488
Java 825.157.28924.959.166 – 25.355.412
Java 922.580.11722.471.102 – 22.689.132
Java 1022.129.42521.889.153 – 22.369.698
Java 1123.657.22823.494.292 – 23.820.165
Java 1223.604.65723.385.208 – 23.824.106
Java 1323.626.04823.473.823 – 23.778.273
Java 1423.599.65823.440.825 – 23.758.490
Performance der String-zu-int-Konvertierung im Laufe der Java-Versionen
Performance der String-zu-int-Konvertierung im Laufe der Java-Versionen

Interessanterweise wurde die Methode Integer.parseInt() in Java 9 deutlich (knapp 10 %) langsamer, mit Java 10 noch einmal 2 % langsamer und ab Java 11 wieder schneller, bleibt aber seither etwa 5 % hinter der Performance von Java 7 und 8 zurück. Um dieses Messergebnis zu bestätigen, habe ich alle (ohnehin intern 25-fach wiederholten) Benchmark-Tests noch einmal ausgeführt – mit ähnlichen Ergebnissen.

Auf der Suche nach der Ursache habe ich zunächst die Quellcodes von Integer.parseInt() aller Java-Versionen verglichen. Versionen 7 und 8 sind identisch. In Java 9 wurde der Code etwas umstrukturiert, z. B. wurden Variablen an anderen Stellen deklariert. Am Algorithmus selbst wurde nichts verändert. Die minimalen Code-Änderungen dürften keine Auswirkung auf die Performance haben. Von Java 9 bis zum Early Access Release von Java 14 gab es keine weitere Änderung, bis auf dass in Java 12 die Zahlenbasis („radix“) in die Fehlermeldung bei nicht parsbaren Zahlen mit aufgenommen wurde.

Um zu überprüfen, ob die Änderungen in Java 9 eine Auswirkung auf die Performance hatten, habe ich die Integer.parseInt()-Quellcodes aus Java 8 und 9 kopiert und diese Kopien mit JMH getestet. Beide waren exakt gleich schnell, an den Code-Änderungen liegt es also nicht. (Dieser Test liegt nicht im GitLab-Repository, da ich nicht weiß, in wie weit ich Java-Quellcodes veröffentlichten darf.)

In einem weiteren Versuch habe ich den Integer.parseInt()-Quellcode mit Java 8 compiliert und die resultierende class-Datei mit Java 9 bis 14 ausgeführt. Dies hat zu einem ähnlichen Ergebnis geführt wie der initiale Performance-Test, d. h. Java 9 und 10 wurden langsamer und Java 11 wieder etwas schneller. Der Grund für die unterschiedlichen Geschwindigkeiten muss also innerhalb der JVM liegen. Falls jemand von euch die genaue Ursache kennt, freue ich mich über einen aufklärenden Kommentar.

Zusammenfassung

Ich habe in diesem Artikel gezeigt, wie ihr Zahlen im Dezimal- und anderen Zahlensystemen parsen könnt und was der Unterschied ist zwischen parseInt() und parseUnsignedInt(). Achtet darauf, nicht unnötig von int nach Integer zu boxen, oder umgekehrt, oder – am schlimmsten – beides hintereinander. Wenn du den Artikel hilfreich findest, freue ich mich, wenn du ihn über einen der folgenden Share-Button teilst.

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.