Ich kann dir die genaue Java-Version jeder Codebase nennen, an der ich je gearbeitet habe — weil ich die meisten davon selbst geschrieben habe. Java 6 für J2ME an der Uni in 2009. Java 7 und 8 bei BluLogix und AFAQ. Java 12 irgendwo dazwischen. Java 17 bei Bytro und danach. Kotlin als Daily Driver seit 2020.
Das hier ist kein Sprach-Spec-Recap. Das ist, was sich für mich als praktizierenden Engineer an den jeweiligen Wendepunkten tatsächlich verändert hat — und wie sich die dunklen Jahre dazwischen angefühlt haben.
Java 6: die J2ME-Ära, a.k.a. Schmerz
Mein erstes Java war J2ME — Java Micro Edition — ich schrieb Mobile Apps für Feature Phones an der Uni, ungefähr 2009. Falls du nie J2ME geschrieben hast, lass mich das Bild malen: keine Generics in der verfügbaren Teilmenge, kein vollständiges Collections-API, eine Screen-Abstraktion, die je nach Gerät variierte, und ein Packaging-Format (JAR als MIDlet), das einen intimem Umgang mit Bytes und Größen lehrte, den moderne Engineers nie kennen.
Die Constraint war der Punkt. Eine J2ME-App, die 64 KB überschritt, wurde von bestimmten Operatoren abgelehnt. Ich schrieb Layout-Code, den ich niemandem zeigen kann ohne Erklärung. Ich debuggte auf dem Gerät, weil Emulatoren falsch genug waren, um einen in die Irre zu führen. Ich shipped Apps, die auf Nokia-Handsets mit 2 MB Heap liefen.
Hier lernte ich, dass die Abstraktionen der JVM nicht kostenlos sind — und dass man sie günstig machen kann, wenn man genau versteht, was man bezahlt. Diese Lektion ist seither jeden Tag nützlich.
Java 7: try-with-resources beendete drei ganze Bug-Klassen
Java 7 erschien 2011. Als ich es beruflich bei BluLogix einsetzte (2013), war
das Upgrade, das mich sofort begeisterte, try-with-resources.
Davor war das Schließen von Ressourcen in Java eine spezifische Steuer: ein finally-Block, der selbst einen Null-Check brauchte, weil die Ressource vielleicht nicht geöffnet wurde, und das Close könnte werfen, und wenn das Close wirft, während man schon in einer Exception ist, verschluckt man die ursprüngliche Exception, und jetzt debuggt man ein Gespenst. Das Pattern war allgemein bekannt und universell nervig:
InputStream is = null;
try {
is = new FileInputStream(path);
// do stuff
} catch (IOException e) {
// handle
} finally {
if (is != null) {
try { is.close(); } catch (IOException ignored) {}
}
}
Mit Java 7:
try (InputStream is = new FileInputStream(path)) {
// do stuff
}
Das ist keine Kleinigkeit. Das ist der Compiler, der einen Ressourcen-Lifecycle erzwingt, der früher Disziplin und Wachsamkeit erforderte. Ich schätze, dass ich mindestens ein Dutzend Resource-Leak-Bugs in älteren Codebases behoben habe, indem ich sie auf try-with-resources migriert habe. Seitdem habe ich keinen mehr geschrieben.
Der Diamond-Operator (<>) kam auch mit Java 7 — man konnte new ArrayList<>()
statt new ArrayList<String>() schreiben. Lächerlich klein im Rückblick. Damals
beseitigte es eine spezifische Reibung, die ich dreißigmal täglich hatte.
Java 8: das, was wirklich umschrieb, wie ich denke
Java 8 (2014) ist der größte Wendepunkt in meiner Java-Karriere. Nicht wegen
Lambdas im Abstrakten. Wegen dem, was Lambdas in der Praxis ermöglichten: das
Streams-API, Optional und Methodenreferenzen — und die Kombination aller drei,
die verändert, wie ich über Datentransformation nachdenke.
Vor Java 8 war das Verarbeiten einer Liste in Java eine for-each-Schleife, ein lokaler Accumulator, Null-Checks auf jeden Zugriff und explizites Collection-Konstruieren. Der Code war korrekt, aber so ausführlich, dass die Absicht auf den ersten Blick schwer zu lesen war.
Nach Java 8:
orders.stream()
.filter(o -> o.getStatus() == ACTIVE)
.map(Order::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Die Logik steckt in der Pipeline. Man liest von oben nach unten, und die Form der Datentransformation ist sichtbar. Die Akkumulation ist implizit. Filter, Map, Reduce sind benannte Operationen mit klarer Semantik.
Was ich tatsächlich bemerkte: Meine Code-Reviews bekamen andere Gespräche. Statt “diese Schleife sieht falsch aus, lass sie uns traceн” hatten wir “warum materialisierst du hier, kannst du lazy bleiben?” und “dieses filter + map kann ein flatMap sein.” Die Primitive verschoben, worüber wir stritten.
Optional war polarisierend. Ist es noch. Ich mag es für Return-Types, wo
Abwesenheit semantisch bedeutsam ist und du den Caller zwingen willst, sich
mit dieser Abwesenheit auseinanderzusetzen. Ich mag Optional nicht als
Feldtyp, Method-Parameter oder Collection-Element. Das ist, denke ich, die
korrekte Position. Zwei Jahre brauchte ich, um vollständig dort anzukommen.
Die Java-9-Module-Ära: legendäres Chaos
Java 9 brachte das Java Platform Module System (JPMS). In der Theorie: ein Weg, explizite Modulgrenzen zu deklarieren, zu kontrollieren, welche Packages dein Modul exponiert, und eine wartbarere große Java-Anwendung zu bauen. In der Praxis: eine sechs-jährige Leidensperiode für jeden mit Dependencies.
Das Problem war das Ökosystem. JPMS verlangte, dass jede Library entweder als
proper named module shippe oder als “automatic module” behandelt wird mit
heuristisch abgeleiteten Modulnamen, die sich zwischen Builds ändern konnten.
Die Mehrheit des Java-Ökosystems war nicht bereit. Das Ergebnis: das Flag
--module-path zum Projekt hinzufügen und dann einen Tag damit verbringen,
Split-Package-Konflikte, unnamed modules und --add-opens-Beschwörungsformeln
für Reflection aufzulösen, auf die Libraries sich verlassen hatten.
Ich habe --add-opens java.base/java.lang=ALL-UNNAMED öfter geschrieben, als
ich zählen kann. Jedes Mal verlässt mich ein kleines Stück meiner Seele.
Java 10, 11, 12 verbesserten die Situation am Rand. Local Variable Type
Inference (var) kam mit Java 10 und ich adoptierte es sofort — nicht weil es
Zeichen spart, sondern weil es verhindert, dass man den Typ zweimal schreibt,
wenn der Konstruktor bereits sagt, was er ist:
var connections = new HashMap<String, ConnectionPool>();
Die Module-Situation verbesserte sich langsam. Spring, Hibernate und die anderen Großen kriegten die Kurve. Als ich bei Java 17 war, war das JPMS-Drama hauptsächlich Hintergrundrauschen statt aktiver Incident.
Java 17: endlich-modernes-Java
Java 17 ist ein LTS-Release (September 2021) und die erste Version, bei der ich Java als Ganzes betrachte und denke: das ist eine moderne Sprache.
Records:
record Point(double x, double y) {}
Das war’s. Immutable Value-Type, equals, hashCode, toString alle
generiert. Kein Lombok. Kein 40-Zeilen-Klasse. Nur die Deklaration, was die
Daten sind. Ich benutze Records jetzt überall — DTOs, Command-Objekte, Event-
Payloads, Domain-Value-Objekte. Die Boilerplate-Kosten für Java-Value-Types
gingen von “nervig” auf “null.”
Sealed Classes + Pattern Matching:
sealed interface Shape permits Circle, Rectangle {}
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
};
Der Compiler weiß, dass Shape nur Circle oder Rectangle sein kann. Wenn
du einen dritten permits-Typ hinzufügst und vergisst, ihn im Switch zu
behandeln, bekommst du einen Compile-Fehler. Das sind algebraische Datentypen
in Java, 25 Jahre nachdem ML sie hatte. Besser spät als nie — und die
Java-Version ist praktisch, idiomatisch und integriert sich sauber ins
bestehende Typsystem.
Text Blocks — mehrzeilige String-Literale ohne Escape-Wahnsinn — kamen mit Java 15 und ich benutze sie ständig für SQL, JSON-Snippets in Tests und jeden String, der mehr als zwei Zeilen umfasst.
Kotlins Aufstieg parallel dazu
Ich will hier ehrlich sein: als Java 17 mit Records und Sealed Types kam,
hatte ich zwei Jahre lang Kotlin bei Bytro geschrieben und das hatte meine
Erwartungen neu kalibriert. Data Classes. Null Safety im Typsystem. Extension
Functions. Coroutines für Async. Smart Casts, die die Hälfte der
instanceof-Ketten eliminieren.
Kotlin ist Javas Schatten-Selbst — was Java hätte sein können, wenn es um 2012 neu designed worden wäre statt 1995. Es läuft auf der JVM, interoperiert mit Java fast perfekt und behebt die meisten Dinge, die Java bis Java 17 braucht, um sie zu beheben.
Ich schreibe Kotlin, wenn ich Kotlin schreiben will. Ich schreibe Java 17, wenn das Projekt Java ist und ich keine Sprach-Dependency einführen will. Beide sind Daily Drivers. Die JVM unter beiden ist dieselbe JVM, was bedeutet: das GC-Tuning- Wissen, die Heap-Diagnose-Skills, das Thread-Dump-Lesen, JMX-Monitoring — alles transferiert. Die JVM ist die dauerhafte Investition; Java und Kotlin sind die Sprachschichten darüber.
Was 15 Jahre auf der JVM wirklich lehren
Die ehrliche Antwort: Du hörst auf, von Allocations überrascht zu werden, und
wirst strategisch mit ihnen. Du entwickelst Meinungen über Garbage-Collector-
Pausen, die du mit Zahlen verteidigen kannst. Du verstehst, warum String-
Interning wichtig ist und genau wann es wichtig ist und wann nicht.
Die Sprachverbesserungen sind real. Java 17 ist dramatisch besser zu schreiben als Java 6. Aber die zugrundeliegenden Skills — das Cost-Modell verstehen, JVM- Diagnostics lesen, über Thread Safety nachdenken, für Testability designen — die ändern sich nicht so sehr wie die Syntax.
J2ME in 2009 lehrte mich Bytes zu zählen. Streams in 2014 lehrten mich in Pipelines zu denken. Records in 2021 sagten mir, die Sprache holt endlich auf, wie ich seit Jahren über Value-Types nachgedacht hatte.
Ich bin noch hier. Die JVM ist noch hier. Java ist jetzt bei Version 21 (LTS). Ich habe Meinungen über Virtual Threads (Project Loom ist real und gut). Ich werde Meinungen über das haben, was mit Java 25 kommt.
Das ist, so weit ich erkennen kann, einfach wie eine 15-jährige Beziehung mit einer Plattform aussieht.