Einleitung
Java und Java-Webentwicklung im Jahr 2012
Java-Webentwicklung im Jahr 2012 war eine ganz andere Welt als das, was die meisten Teams heute gewohnt sind. Es gab keine Stream-API, daher bedeutete die tägliche Verarbeitung von Collections explizite Schleifen, Hilfsmethoden und jede Menge Boilerplate. Unveränderlichkeit war auch nicht die Standarddenkweise – der meiste Code stützte sich stark auf veränderlichen Zustand. Webanwendungen wurden typischerweise rund um Servlets gebaut und auf vollständigen Application Servern deployt, und Innovation fühlte sich langsamer an, einfach weil die Release-Zyklen länger waren.
Gleichzeitig hatte Java einige unbestreitbare Stärken. Die Sprache war stabil und vorhersagbar. Die IDE-Unterstützung war (und ist immer noch) erstklassig, was das Navigieren und Refactoring großer Codebasen viel einfacher machte. Die Performance war schnell und konsistent, Debugging war im Vergleich zu dynamischeren Umgebungen generell unkompliziert, und das Ökosystem war riesig. Wenn Sie erfahrene Entwickler brauchten, die wussten, wie man ernsthafte Systeme baut und betreibt, hatte Java sie.
Weil wir Java ziemlich mochten – und weil Webentwicklung mit Servlets und Application Servern schmerzhaft langsam und nervig war, starteten wir Ninja im Jahr 2010. Ninja war eines der ersten Web-Frameworks, die versuchten, ein Rails-artiges Gefühl von Produktivität und Developer Experience zu Java zu bringen. Spring Boot existierte damals noch nicht. Das einzige andere coole (unserer Meinung nach) Java-Web-Framework war Play 1. Aber wir hatten Bedenken, da es zu viel Bytecode-Magie verwendete und dabei war, nach Scala zu migrieren.
Ninja existiert heute noch, aber es trägt viele der Paradigmen und Annahmen aus dieser Ära: Veränderlichkeit als Standard, keine Framework-seitige Unterstützung von Lambdas und ein gutes Maß an impliziter “Magie”, um Controller und Routen sauber zu verdrahten. Es gibt auch eigene Implementierungen an Stellen, wo modernes Java (und moderne Bibliotheken) die Dinge natürlicher handhaben würden – wie UTF-8-Unterstützung in Properties und Messages.
Java in 2026
In der Java-Welt hat sich seit 2012 viel verändert.
Am bemerkenswertesten:
- Java-Releases erscheinen jetzt viel häufiger, und wir sind bereits bei Java 25.
stream()und Operationen wiemap()fügen Vorhersagbarkeit und einen funktionalen Charakter zum alltäglichen Code hinzu.- Unveränderlichkeit und
Optionalsind Teil des Mainstream-Java-Werkzeugkastens. - Wir haben mehrzeilige Strings – endlich. (Noch keine Template Strings allerdings.)
- Lambdas sind dramatisch einfacher zu verwenden, einschließlich praktischer Methodenreferenzen.
- Records machen es unkompliziert, Datenklassen mit korrekten
equals()- undhashCode()-Implementierungen zu erstellen.
Eine Suche
Ich wollte Ninja schon lange modernisieren – aber es gab viel Ballast, und ein einfaches Refactoring fühlte sich nicht realistisch an. Und da es immer “nützlich” ist, etwas leicht Dummes zu tun, wenn man lernen will, beschloss ich, Ninja von Grund auf für modernes Java neu zu schreiben.
Hier sind meine Beobachtungen zu modernem Java während meiner Neufassung von Ninja.
Beobachtungen zu modernem Java
Mehrzeilige Strings – aber keine Template Strings
Java hatte sehr lange keine mehrzeiligen Strings. Aber jetzt haben wir sie:
String myString = """
A long
and funky
String
""";
Endlich. Was. Für. Eine. Erleichterung.
Leider – haben wir noch keine Template Strings. Aaahhhhhh.
So etwas existiert einfach nicht. In 2026. Schade.
String name = "a name";
// The following does not exist as of 2026:
String myTemplate = """
Hi {{name}}!
""";
Das ist nicht schön. Ich würde das als Grundfunktion einer Programmiersprache erwarten. Es gibt viele Diskussionen um dieses Thema.
Aber Stand Java 25 ist es nicht im JDK.
Über funktionale unveränderliche Programmierung
Stream API
Die Stream API funktioniert überraschend gut. Ok. Man muss immer .stream() aufrufen und hat oft auch einen Collector. Aber es ist eine Freude zu benutzen und fühlt sich für viele Operationen so viel besser an als eine for-Schleife.
Null-sichere Programmierung und Optional
Im Jahr 2026 denken die meisten Menschen, dass null ein Fehler war (manche nicht).
Ich persönlich denke wirklich, dass null ein Fehler ist. Oder genauer – eine Sprache, die nicht garantieren kann, ob etwas null ist. Viele moderne Sprachen haben so etwas:
var person: Person // person is guaranteed by the compiler that it never can become null.
var person: Person? // ...? to signal that it can be null
Dass die Sprache das für Sie übernimmt, ist so viel besser und sicherer.
Aber was Java hat, ist Optional. Und neuere Frameworks verwenden ohnehin kein null. Also entweder Person oder Optional<Person> zurückgeben. Funktioniert für mich und ist der Weg für Java in 2026. Aber auch hier ist es fehleranfälliger, als einfach zur Kompilierzeit zu verbieten, dass eine Variable überhaupt null werden kann.
Über Module
Ja. Ich verwende keine Module. Und ich habe keinen Anwendungsfall dafür. Scheint akademisch.
Switch-Case-Blöcke
Irgendwann hat Java stark verbesserte Switch-Case-Anweisungen und Sealed Classes eingeführt. Das kenne ich von Kotlin und Scala, und diese sind wichtig, um Code lesbarer und vorhersagbarer zu machen.
Ein Praxisbeispiel sehen Sie in der neuen Version von Ninja hier. Die Handhabung von Client-seitigen Sessions ist etwas knifflig, da sie auf der Cookie-Semantik des Browsers basieren.
Hier kommt ein Sealed Interface total gelegen:
public sealed interface NinjaSessionState permits Exists, Remove, UnknownButDontTouch {
}
public static final class Exists implements NinjaSessionState {
private final NinjaSession session;
public Exists(NinjaSession session) {
this.session = Objects.requireNonNull(session, "session");
}
public NinjaSession getSession() {
return session;
}
}
public static final class Remove implements NinjaSessionState {
// No session field!
}
public static final class UnknownButDontTouch implements NinjaSessionState {
// No session field!
}
… und das führt dann zu stark verbesserter Switch-Logik. Beachten Sie, dass Sie die Session (exists.getSession()) extrahieren können, wenn wir einen Result.Exists-Session-Zustand erhalten.
switch (result.ninjaSessionState()) {
case Result.Exists exists -> {
NinjaSession ninjaSessionForResponse = exists.getSession();
var cookie = ninjaSessionConverter.createCookieWithInformationOfNinjaSession(ninjaSessionForResponse);
httpServletResponse.addCookie(NinjaJettyHelper.convertNinjaCookieToServletCookie(cookie));
}
case Result.Remove remove -> {
var cookie = ninjaSessionConverter.createCookieToRemoveNinjaSession();
httpServletResponse.addCookie(NinjaJettyHelper.convertNinjaCookieToServletCookie(cookie));
}
case Result.UnknownButDontTouch unknown -> {
// Intentionally don't do anything
}
}
Schön!
Über Records
Records funktionieren sofort. Und für einfache Datenklassen ist das nett.
Meine größten Bedenken sind zweifacher Natur:
getXYZ-Pattern gebrochen
Ein Record wie
public record Person(String firstName, String lastName) {}
erlaubt Ihnen den Zugriff auf den Namen via
var person = new Person("firstName", "lastName");
person.firstName(); // not getFirstName()
Das ist hässlich. Java-Entwickler wissen, wie man auf Properties zugreift: Einfach “get” in der IDE tippen und Code-Completion erhalten. Letztlich kennt man als Nutzer einer API die Implementierungsdetails (Klasse / Record) nicht. Aber dieses Pattern ist gebrochen.
Noch seltsamer: Wenn ich mehr Funktionalität wie fullName hinzufügen möchte, müsste ich dann getFullName() oder fullName() verwenden?
record Person(String firstName, String lastName) {
public String getFullName() {
return firstName + " " + lastName;
}
}
Kein automatisches Builder-Pattern
Das Builder-Pattern ist recht nützlich, wenn Sie viele Properties in Ihrem Record haben.
var person = Person.builder().firstName(...).lastName(...).xyz(...).build();
Aber Java (bzw. Records) erstellt diesen Builder nicht für Sie. Also haben Sie entweder lange Instanziierungen wie diese:
var person = new Person(.. ,.. ,... ,... );
… was natürlich schwer zu lesen und sehr fehleranfällig ist.
Oder Sie implementieren den Builder von Hand (oder via Lombok). Das finde ich nicht ideal.
Keine Standardwerte für Klassen und Records
In anderen Sprachen gibt es Standardwerte und auch die Möglichkeit, Werte Parametern einer Methode zuzuweisen:
// ... This would be something I'd imagine in Java
class MyPerson(String firstName, String lastName, Optional<String> middleName = Optional.empty())
// of course the following also does not work in Java 25
new MyPerson(firstName = "a first name", lastName = "")
Standardwerte und das Zuweisen von Werten erlauben es, drei Dinge auf einmal zu lösen:
- Weniger Fehler beim Erstellen neuer Klassen
- Kein Builder-Pattern nötig
- Kein Bedarf an vielen sich wiederholenden Default-Konstruktoren
Ich hätte erwartet, dass Java das 2026 hat. Aber vergebens. Das ist eine kleine Enttäuschung.
Java-Talente mit Backend-Fähigkeiten zu finden ist einfach
Ein übersehenes Merkmal von Java ist die sehr hohe Talentdichte für Backend-Entwickler.
Das ist oft bei Sprachen wie Python (starke Neigung zu KI mit weniger Erfahrung beim Aufbau von Backend-Systemen) und Go (viele Leute arbeiten gerne im Kubernetes-Bereich) nicht der Fall.
Das ist eindeutig ein Pluspunkt für Java. Und das ist auch 2026 noch so.
IDEs sind immer noch großartig
Java wurde so gebaut, dass es sehr IDE-freundlich ist. Und das zeigt sich immer noch.
Meine Haupt-IDE ist NetBeans. Und es ist eine solche Freude zu benutzen. Debugging ist kinderleicht. Einen Bug gefunden? Debugger starten. Test funktioniert nicht? Debuggen. Ein Klick genügt.
Code-Completion ist eine Freude. Und die Compile-on-Save-Funktion ermöglicht Ninjas Superdev-Modus.
Während andere Sprachen auch gute IDEs und Debugger haben, ist mein subjektives Gefühl, dass Java einfach ein bisschen einfacher zu benutzen ist, was Sie in der täglichen Arbeit produktiver macht.
Schnelle Kompilierungszeiten und schnelle Starts
Java hat den Ruf, langsam zu sein. Aber für meine Anwendungsfälle, und für das, was es bietet, ist das nicht der Fall. Der Compiler ist sehr schnell und reaktionsschnell.
Ich verwende hauptsächlich Maven und NetBeans als IDE. Das Feedback und die Kompilierung sind auf meinem M1 Mac fast sofort. Es gibt keinerlei Produktivitätseinbußen durch Java.
Das Starten einer vollständigen Ninja-Anwendung (einschließlich Datenbankmigrationen und einem vollständigen HTTP-Server) dauert etwa 300ms auf einem M1 Mac.
Das ist ein nicht optimierter Start ohne jegliche Magie. Das ist schnell genug selbst für Pods, die hochskaliert werden. Ein langsamerer Request ist schlecht, aber 300ms ab und zu ist für die meisten Nutzer akzeptabel.
Natürlich könnte man das noch weiter optimieren, z.B. durch Class Data Sharing oder GraalVM.
Unterstützung für agentische KI
Ich habe viel mit Claude Code und OpenCode während des Refactorings experimentiert. Beide funktionieren sehr gut mit Maven + Java-Codebasen. Sie “verstehen” den mvn-test-Build-Zyklus. Und sie “verstehen” auch Refactorings über Module hinweg oder komplexere Strukturen. (Meistens, wie immer mit agentischer KI zu diesem Zeitpunkt).
Fazit
Ich habe es genossen, mit Java an der Neufassung zu arbeiten. Die Entwicklererfahrung ist immer noch sehr gut und die hinzugefügten Features bringen viele Verbesserungen für die Sprache.
Es ist auch großartig zu sehen, dass die IDEs erstaunlich sind, Kompilierungszeiten kein Problem darstellen und das Ökosystem immer noch floriert.
Andererseits – Kotlin ist für mich immer noch die “schönere” Sprache, wenn es um JVM-Sprachen geht. Aber das Gesamtpaket von Java im Vergleich ist auch nicht schlecht.
Und besonders wenn Sie entscheiden müssen, ob Sie Kotlin (nicht so weit verbreitet) für ein neues Projekt oder Java (weit verbreitet und viele Talente) verwenden, ist es jetzt einfacher denn je, Java zu wählen. Ihre Erfahrung mag variieren. Und Golang ist auch cool.
