Zum Inhalt springen

Java: String in int umwandeln – Besonderheiten und Fallstricke

Eine hohe, kobaltblau leuchtende Neon-Ziffer 7 links vor Senfgelb; rechts davon, nach einer Lücke, eine Reihe von sieben leuchtend weißen Glaskugeln auf dem Boden

Im vorherigen Artikel habe ich dir gezeigt, dass es seit Java 9 keine Rolle mehr spielt, ob du ein int mit String.valueOf(i) oder mit "" + i in einen String umwandelst – und dass String.format() dafür die falsche Methode ist.

Heute geht es in die entgegengesetzte Richtung: Was ist beim Parsen von Strings in ints zu beachten? Den Quellcode zum Artikel findest du in meinem GitHub-Repository.

Zahlen im Dezimalsystem parsen

Schauen wir uns zunächst einmal die Möglichkeiten an, einen String in ein int (bzw. Integer) zu parsen. Die beiden klassischen Methoden, die es seit Java 1.0 gibt:

  • 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 Minuszeichen. Folgendes ist also erlaubt:

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

Folgende Aufrufe hingegen führen zu einer NumberFormatException:

  • Integer.parseInt("") // Leerer String nicht erlaubt
  • Integer.parseInt(null) // NumberFormatException, keine NullPointerException
  • Integer.parseInt(" 1") // Leerzeichen nicht erlaubt
  • Integer.parseInt("3,14") // Komma nicht erlaubt
  • Integer.parseInt("1.000") // Tausender-Punkt nicht erlaubt
  • Integer.parseInt("1_000") // Unterstrich nur im Literal

Den Unterstrich als Tausendertrenner erlaubt Java seit Version 7 nur in Zahlenliteralen wie 1_000 – für Integer.parseInt() bleibt er ein ungültiges Zeichen.

Hexadezimal- und Binärzahlen parsen

Die zuvor genannten Methoden parsen Dezimalzahlen. Um andere Zahlensysteme zu parsen, stehen folgende überladene Methoden 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 du z. B. so:

  • Integer.parseInt("CAFE", 16)

Und eine Binärzahl so:

  • Integer.parseInt("101111", 2)

Seit Java 9 gibt es außerdem Integer.parseInt(s, beginIndex, endIndex, radix) (→ JavaDoc). Die Methode parst einen Ausschnitt aus dem String, ohne dass du vorher mit substring() ein neues String-Objekt erzeugen musst: Integer.parseInt("Port: 8080", 6, 10, 10) liefert 8080. Für parseUnsignedInt(), das ich dir im nächsten Abschnitt zeige, gibt es dieselbe Variante.

Integer.decode() (→ JavaDoc) erkennt die Basis am Präfix: Integer.decode("0xCAFE") und Integer.decode("#CAFE") ergeben beide 51.966. Vorsicht bei einer führenden Null: Wie im Java-Quellcode steht sie für eine Oktalzahl. Integer.decode("010") ergibt deshalb 8 – und Integer.decode("09") wirft eine NumberFormatException, weil 9 keine Oktalziffer ist.

Java 17 hat mit HexFormat (→ JavaDoc) eine weitere Klasse gebracht, gedacht vor allem für die Umwandlung zwischen byte-Arrays und hexadezimalen Strings: HexFormat.of().parseHex("cafebabe") liefert ein byte-Array mit vier Bytes. Für einzelne Zahlen hat sie statische Methoden: HexFormat.fromHexDigits("CAFE") liefert 51.966, HexFormat.fromHexDigitsToLong(s) ein long, und eine Variante mit Start- und Endindex parst einen Ausschnitt. Anders als parseInt() akzeptiert sie weder Vorzeichen noch Präfix – 0x führt zu einer NumberFormatException –, sondern höchstens acht Hex-Ziffern, die sie als Bitmuster liest. Was das für CAFEBABE bedeutet, siehst du im nächsten Abschnitt.

Signed vs. unsigned ints

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

Interessant (um nicht zu sagen: verwirrend) wird es, wenn du z. B. den gültigen int-Wert 0xCAFEBABE (die magische Zahl für Java-Klassendateien) in einen hexadezimalen String umwandelst 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 (den Zusatz „under radix 16“ gibt es seit Java 12):

java.lang.NumberFormatException: For input string: "cafebabe" under radix 16

Warum ist das so?

Zunächst einmal: Die Variable 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. Konvertierst du cafebabe ins Dezimalsystem, erhältst du 3.405.691.582. Diese Zahl ist größer als Integer.MAX_VALUE und kann somit nicht als int dargestellt werden.

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 ich hier der Übersicht halber eingefügt). Hexadezimal wird diese negative Zahl als positiver Wert cafebabe dargestellt, der 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 int in Java nun eben immer signed (vorzeichenbehaftet) ist: -889.275.714, also denjenigen int-Wert, den wir auch erhalten, wenn wir 0xCAFEBABE einem int zuweisen. Dasselbe Ergebnis liefert übrigens HexFormat.fromHexDigits(s) aus Java 17: Die Methode liest die acht Hex-Ziffern als Bitmuster und fragt nicht nach dem Vorzeichen.

Und wie kommen wir an die 3.405.691.582? Seit Java 8 direkt aus dem int: Integer.toUnsignedLong(i) (→ JavaDoc) liefert 3.405.691.582 als long, Integer.toUnsignedString(i) als String. Oder du parst cafebabe (oder CAFEBABE – die Groß-/Kleinschreibung ist unbedeutend) gleich in ein long:

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). Die folgende Grafik zeigt das Bitmuster von 0xCAFEBABE und seine beiden Lesarten:

Das Bitmuster 0xCAFEBABE in 32 Bit-Zellen mit gesetztem Bit 31; als int gelesen -889.275.714, als long gelesen 3.405.691.582
Ein Bitmuster, zwei Zahlen: als int -889.275.714, als long 3.405.691.582

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 du hier in meinem GitHub-Repository findest und das verschiedene (Grenz-)Werte mit beiden Methoden parst.

Die beiden Methoden decken unterschiedliche Wertebereiche ab, die sich von 0 bis Integer.MAX_VALUE überlappen:

Wertebereiche von parseInt() und parseUnsignedInt() als Zahlenstrahl: parseInt() von -2³¹ bis 2³¹-1, parseUnsignedInt() von 0 bis 2³²-1; von 0 bis 2³¹-1 liefern beide dieselben Ergebnisse, darüber liefert parseUnsignedInt() negative ints
Von 0 bis Integer.MAX_VALUE liefern beide Methoden dasselbe Ergebnis – darüber liefert parseUnsignedInt() negative ints

Hier das Ergebnis des Programms zusammengefasst (✗ steht für eine NumberFormatException):

StringBemerkungparseInt()→ HexparseUnsignedInt()→ Hex
-2147483649MIN_VALUE - 1✗✗✗✗
-2147483648MIN_VALUE-214748364880000000✗✗
-1000000000-1000000000c4653600✗✗
-1-1ffffffff✗✗
00000
100000000010000000003b9aca0010000000003b9aca00
2147483647MAX_VALUE21474836477fffffff21474836477fffffff
2147483648MAX_VALUE + 1✗✗-214748364880000000
3000000000✗✗-1294967296b2d05e00
42949672952 * MAX_VALUE + 1✗✗-1ffffffff
42949672962 * 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über hinaus 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. Das ist der Unterschied zum ersten Fall: Dort gibt es eine Umwandlung, und die ist nötig – ein Integer entsteht nur durch Boxing. Hier sind es zwei, die sich gegenseitig aufheben. Genau dieses Paar meldet IntelliJ als redundant und empfiehlt, valueOf() durch parseInt() zu ersetzen:

Screenshot der IntelliJ-Warnung „Redundant boxing“ an Integer.valueOf() mit dem Vorschlag, Integer.parseInt() zu verwenden
IntelliJ warnt vor redundantem Boxing

Ob HotSpot uns diesen Umweg verzeiht, prüfen wir im nächsten Kapitel mit dem Profiler.

Performance der String-zu-int-Umwandlung

Gemessen habe ich mit dem Java Microbenchmark Harness (JMH), mit demselben Aufbau wie im Schwesterartikel: zwei Rechner – ein Dell XPS 17 mit Intel Core i7-12700H (x86) unter WSL2 und ein Mac mit Apple M5 Pro (arm64) –, Java 8 bis 27, drei Forks mit je fünf Aufwärm- und fünf Messiterationen, dazu der GC-Profiler für die allokierten Bytes pro Aufruf. Jede Version habe ich mehrfach gemessen, fünfmal auf dem i7 und viermal auf dem M5 Pro; die Tabellen zeigen den Mittelwert. Warum das nötig ist und wann ich einen Unterschied zwischen zwei Versionen überhaupt einen Unterschied nenne, steht in der Info-Box im Schwesterartikel.

Drei Fragen wollte ich beantworten:

  1. Unterscheiden sich die Parse-Methoden – positive, negative, vorzeichenlose Zahlen?
  2. Was kostet das Boxing in ein Integer?
  3. Wie hat sich Integer.parseInt() von Java 8 bis 27 entwickelt?

Der Benchmark

Den Benchmark findest du in meinem GitHub-Repository. Hier ein Ausschnitt mit drei der acht Methoden:

public class StringToIntMultiBenchmark {

  @Benchmark
  public void integerParsePositiveInt(RandomNumberStrings state,
      Blackhole blackhole) {
    int i = Integer.parseInt(state.positive(state.next()));
    blackhole.consume(i);
  }

  @Benchmark
  public void integerParsePositiveInteger(RandomNumberStrings state,
      Blackhole blackhole) {
    Integer i = Integer.valueOf(state.positive(state.next()));
    blackhole.consume(i);
  }

  @Benchmark
  public void integerParsePositiveIntegerAndUnbox(RandomNumberStrings state,
      Blackhole blackhole) {
    int i = Integer.valueOf(state.positive(state.next()));
    blackhole.consume(i);
  }

}

Der State liefert 1.024 zufällige siebenstellige Zahlen als Strings – einmal pur, einmal mit Plus, einmal mit Minus – und wird einmal pro Messlauf gefüllt:

@State(Scope.Thread)
public class RandomNumberStrings {

  private static final int SIZE = 1024;

  private final String[] positive = new String[SIZE];
  private final String[] positiveWithPlus = new String[SIZE];
  private final String[] negative = new String[SIZE];
  private int index;

  @Setup(Level.Trial)
  public void doSetup() {
    ThreadLocalRandom random = ThreadLocalRandom.current();
    for (int k = 0; k < SIZE; k++) {
      String digits = Integer.toString(1_000_000 + random.nextInt(9_000_000));
      positive[k] = digits;
      positiveWithPlus[k] = "+" + digits;
      negative[k] = "-" + digits;
    }
  }

  public int next() {
    return index++ & (SIZE - 1);
  }

  public String positive(int k) {
    return positive[k];
  }

  // positiveWithPlus(int k) und negative(int k) analog

}

Warum die Zahlen einmal pro Messlauf gezogen werden und nicht vor jedem Aufruf, erkläre ich in der Info-Box im Schwesterartikel. Kurzfassung: Ein Parse-Aufruf dauert fünf bis acht Nanosekunden, und ein Setup pro Aufruf würde JMH zwingen, jeden einzelnen Aufruf mit Zeitstempeln zu umrahmen – dann misst man die Uhr statt des Parsers. Die Messungen der ersten Fassung dieses Artikels hatten genau diesen Fehler.

Die Parse-Methoden im Vergleich

Alle acht Methoden auf Java 27: die Zeit pro Aufruf in Nanosekunden, gemittelt über zwei JMH-Läufe pro Rechner mit je drei Forks und fünf Messiterationen, und, in der letzten Spalte, die Bytes, die ein Aufruf allokiert – die Zahl, auf die es im übernächsten Abschnitt ankommt. Die Rohdaten mit Fehlerintervallen liegen als JMH-JSON im results/-Verzeichnis des Benchmark-Repositorys.

Methodex86arm64Bytes pro Aufruf
parseInt(), positiver Wert7,45,40
parseInt(), positiver Wert mit Plus7,15,40
parseInt(), negativer Wert7,05,20
parseUnsignedInt(), positiver Wert6,95,10
parseUnsignedInt(), positiver Wert mit Plus7,45,20
parseInt() mit anschließendem Boxing7,55,816
valueOf(), positiver Wert7,65,816
valueOf() mit anschließendem Unboxing7,35,40

Das Diagramm zeigt die Zeiten; die Bytes stehen nur in der Tabelle.

Die acht Parse-Methoden auf Java 27 – Nanosekunden pro Aufruf auf beiden Rechnern
Die Parse-Methoden auf Java 27 – Nanosekunden pro Aufruf

Die fünf Zeilen ohne Boxing – parseInt() und parseUnsignedInt() in allen Vorzeichen-Varianten – liegen dicht beieinander: 6,9 bis 7,4 ns auf dem i7, 5,1 bis 5,4 ns auf dem M5 Pro. Auf jedem der beiden Rechner liegen die fünf Werte also innerhalb einer halben Nanosekunde, und zwei Läufe pro Methode können in einem so schmalen Band keine Rangfolge belegen. Dass die Unterschiede Rauschen sind und keine Eigenschaft der Methoden, siehst du auch daran, dass die Reihenfolge auf den beiden Rechnern nicht dieselbe ist: parseInt() mit positivem Wert ist auf dem M5 Pro die langsamste der fünf, parseUnsignedInt() mit Plus auf dem i7.

Das ist auch zu erwarten, denn der Code dahinter ist praktisch derselbe. parseUnsignedInt() hat zwar eine eigene Ziffernschleife, sie unterscheidet sich von der in parseInt() aber nur in der Überlaufprüfung – die eine rechnet vorzeichenbehaftet, die andere vorzeichenlos. Und ob ein Plus, ein Minus oder gar kein Vorzeichen vor der Zahl steht, kostet eine Verzweigung, die der Branch Predictor nach dem ersten Durchlauf kennt.

Was Boxing kostet

Die drei Zeilen mit Boxing – parseInt() mit anschließendem Boxing, valueOf() und valueOf() mit anschließendem Unboxing – erzählen eine andere Geschichte als die erste Fassung dieses Artikels.

Integer.valueOf(s) liefert ein Integer-Objekt. Für Werte außerhalb des Caches von -128 bis 127 – also für alle unsere siebenstelligen Zahlen – muss es allokiert werden: 16 Bytes pro Aufruf, die der GC-Profiler auch zeigt. Auf dem M5 Pro kostet das 7 % Zeit – 5,8 statt 5,4 ns –, auf dem i7 zwei bis vier Prozent. Ob du Integer.valueOf(s) schreibst oder Integer i = Integer.parseInt(s): Es ist dasselbe Boxing, mit denselben 16 Bytes.

Und der Umweg int i = Integer.valueOf(s), also Boxing mit sofortigem Unboxing? Der kostet nichts. Null Bytes pro Aufruf, und die Zeit ist von parseInt() nicht zu unterscheiden. HotSpot erkennt, dass das Integer-Objekt die Methode nie verlässt, und lässt die Allokation weg – das nennt sich Escape-Analyse.

Die Empfehlung von 2019 – „achte darauf, nicht unnötig zu boxen“ – bleibt trotzdem richtig, nur aus einem anderen Grund: Lesbarkeit. Wer ein int braucht, schreibt parseInt(); wer ein Integer braucht, valueOf(). Die IntelliJ-Warnung von oben hat also recht, auch wenn HotSpot den Umweg wegoptimiert.

Integer.parseInt() von Java 8 bis 27

Die drei Boxing-Varianten rufen am Ende Integer.parseInt() auf, und die fünf Varianten ohne Boxing liegen, wie oben gezeigt, dicht beieinander. Deshalb habe ich stellvertretend Integer.parseInt() mit positivem Wert über die Java-Versionen gemessen – Nanosekunden pro Aufruf:

ArchitekturJava 8Java 11Java 17Java 21Java 22Java 23Java 24Java 25Java 27
x8613,99,29,08,27,87,27,27,57,4
arm647,25,65,96,05,45,45,35,35,5
Integer.parseInt() – Nanosekunden pro Aufruf von Java 8 bis 27 auf beiden Rechnern
Integer.parseInt() – Nanosekunden pro Aufruf, Java 8 bis 27

Den großen Schritt siehst du zwischen Java 8 und 11: 34 % weniger Zeit auf x86, 22 % auf arm64. Danach gehen die beiden Rechner verschiedene Wege. Auf dem i7 fällt die Zeit bis Java 23 in jedem Schritt – um 3, 9, 5 und 7 % –, danach passiert nichts mehr, was sich mit fünf Durchläufen vom Messrauschen unterscheiden ließe: Java 23, 24, 25 und 27 liegen innerhalb ihrer Streuung.

Auf dem M5 Pro geht es auf und ab: Java 17 ist 5 % langsamer als 11, Java 21 noch einmal 3 % langsamer, Java 22 macht das mit 11 % mehr als wett – und Java 27 ist 3,7 % langsamer als Java 25, über vier Durchläufe hinweg eindeutig. Von Java 8 bis 27 hat sich die Zeit für parseInt() auf x86 fast halbiert, auf arm64 um ein Viertel verringert.

Am Parser selbst hat sich in dieser Zeit wenig getan. Der größte Teil des Gewinns kommt von den Compact Strings (JEP 254), mit denen Java 9 Strings aus Latin-1-Zeichen in einem byte-Array statt einem char-Array speichert – halb so viele Bytes pro Zeichen, die der Parser lesen muss. Das lässt sich nachmessen, indem man Compact Strings mit der JVM-Option -XX:-CompactStrings abschaltet – in einer eigenen Messreihe, jeweils gegen einen Lauf mit eingeschalteten Compact Strings: parseInt() braucht dann auf dem M5 Pro 6,8 statt 5,7 ns auf Java 11 und 6,8 statt 5,4 ns auf Java 25 – fast wieder das Java-8-Niveau von 7,2 ns.

Der Rest des Gewinns kommt vom JIT-Compiler – und von dort müssen auch die kleinen Verlangsamungen auf dem M5 Pro auf Java 17, 21 und 27 kommen; untersucht habe ich sie nicht. Die erste Fassung dieses Artikels hatte übrigens gemessen, dass parseInt() in Java 9 und 10 langsamer geworden sei – das war der Messfehler mit dem Setup pro Aufruf, nicht Java.

Zusammenfassung

Zahlen im Dezimalsystem parst du mit parseInt(), andere Zahlensysteme über den radix-Parameter, Teilstrings seit Java 9 über die Variante mit Start- und Endindex. parseUnsignedInt() deckt den Bereich bis 4.294.967.295 ab – liefert aber oberhalb von Integer.MAX_VALUE einen negativen int, dessen Bitmuster dem Eingabewert entspricht.

Alle Parse-Methoden sind gleich schnell: fünf bis acht Nanosekunden je nach Rechner. Boxing in ein Integer allokiert 16 Bytes pro Aufruf und kostet zwei bis sieben Prozent Zeit, je nach Rechner; den Umweg über Boxing und Unboxing in ein int optimiert HotSpot weg. Ich empfehle trotzdem: parseInt() für ein int, valueOf() für ein Integer – nicht wegen der Nanosekunden, sondern weil der Code dann sagt, was er tut.

Was soll ich als Nächstes vertiefen? Das erfahre ich am besten durch dein Feedback: Eine Bewertung auf meinem ProvenExpert-Profil zeigt mir, welche Themen dir wichtig sind – und motiviert mich, weitere Artikel zu schreiben.

👉 Bewertung abgeben

Lust auf noch mehr Wissen?

In meinem Blog findest du viele Artikel zu Java, Softwarearchitektur und Performance von grundlegenden Konzepten bis hin zu fortgeschrittenen Patterns.

Wenn du tiefer einsteigen willst, schau dir meine Trainings an: praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Zu den Java-Schulungen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche