Zum Inhalt springen

JEP 500: Final heißt final – Deep Reflection ab Java 26

Ein stählernes Vorhängeschloss vor violettem Hintergrund; am rechten Übergang vom Bügel zum Gehäuse verschweißt ein glühender Schweißpunkt den Bügel mit dem Körper, Funken sprühen nach rechts

Stell dir vor, du startest eine Anwendung, und 2 + 2 ergibt plötzlich 5. Kein Tippfehler, kein kaputter Compiler – drei Zeilen in einer fremden Library haben das gecachte Integer-Objekt 4 heimlich auf 5 umgeschrieben. Per Deep Reflection, über ein finales Feld innerhalb der Integer-Klasse, das eigentlich unveränderlich sein sollte.

Das ist ein netter Partytrick. Gefährlich wird er, wenn das Ziel nicht Integer.value ist, sondern z. B. die Berechtigungen des gerade angemeldeten Accounts.

Genau hier setzt JEP 500: Prepare to Make Final Mean Final an. Er ist Teil von Java 26 (erschienen im März 2026) und beginnt damit, ein über 20 Jahre altes Versprechen einzulösen: Ein finales Feld soll wirklich final sein – nicht nur ein Hinweis an alle, die den Quellcode lesen, sondern eine Zusage, die die JVM einhält.

In diesem Artikel erfährst du:

  • Wie ein harmloses Dependency-Update per Deep Reflection deine Zugriffskontrolle aushebeln kann
  • Was JEP 500 dagegen tut und mit welchen Optionen du das Verhalten steuerst
  • Welche Felder JEP 500 nicht schützt
  • Warum das OpenJDK-Team finale Felder absichert: Integrity by Default und Performance
  • Wie du herausfindest, ob deine Anwendung finale Felder mutiert
  • Was du tun solltest, bevor das Verbot in einer zukünftigen Java-Version zum Standard wird

Den Demo-Code zu diesem Artikel findest du in zwei GitHub-Repositories: der Anwendung und der angreifenden Library.

Wie Deep Reflection finale Felder überschreibt

Der Trick mit 2 + 2 = 5 besteht aus drei Zeilen:

Field value = Integer.class.getDeclaredField("value");
value.setAccessible(true);
value.set(4, 5);

Integer.value ist private und final. setAccessible(true) hebelt beides auf einmal aus – die Kapselung durch private und die Unveränderlichkeit durch final. Danach weist set() dem finalen Feld einen neuen Wert zu, obwohl das per Sprachregel unmöglich sein sollte.

Weil Java den Integer 4 im Cache hält und wiederverwendet, liefert ab jetzt jede Rechnung, deren Ergebnis geboxt wird, das manipulierte Objekt – und damit den Wert 5. Integer sum = 2 + 2 ergibt also 5, int sum = 2 + 2 dagegen weiterhin 4.

Seit Java 16 ist für diesen Trick --add-opens java.base/java.lang=ALL-UNNAMED erforderlich, denn die Klassen des JDK liegen seit Java 9 in benannten Modulen, und dort schützt sie die Strong Encapsulation.

Für Klassen auf dem Classpath hingegen gilt dieser Schutz nicht. Dort liegt – sofern du nicht das Modulsystem verwendest – jeglicher Code deiner Anwendung (einschließlich Frameworks wie Spring), und alle Pakete auf dem Classpath sind offen: Jeder Code darf per Deep Reflection auf ihre nicht-öffentlichen Klassen, Methoden und Felder zugreifen – genau das, was --add-opens für ein Paket des JDK (im Beispiel oben für java.lang) erst erlauben muss.

Für deine eigenen Klassen und die deiner Libraries heißt das: final war bisher keine Garantie. Jede Klasse konnte ein finales Feld überschreiben – ohne Warnung und ohne Eintrag im Log.

Es braucht keinen Exploit – nur eine Dependency

Der Integer-Trick ist eine Spielerei. Das eigentliche Problem zeigt sich, wenn fremder Code ein finales Feld in deiner Anwendung überschreibt.

Dazu eine kleine Spring-Boot-Anwendung mit Login und einem geschützten Admin-Bereich. Der Controller sichert /admin per Methoden-Security ab – nur Nutzer:innen mit der ADMIN-Rolle haben Zugang zu diesem Bereich:

@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String admin() {
  return "admin";
}

Wir melden uns als alice an – eine Nutzerin ohne Rollen – und rufen /admin auf. Das Ergebnis ist, was wir erwarten:

HTTP 403 Forbidden

Die Absicherung durch Spring Security greift. So weit, so gut.

Die Anwendung nutzt eine kleine Observability-Library, die misst, wie lange jeder Request dauert – io.metricstack:request-timing in Version 1.0. Ein Zeitmess-Tool, völlig unverdächtig.

Dann erscheint ein Patch-Update: 1.0 → 1.0.1. Ein Patch-Release, das man ohne nachzudenken durchwinkt – oder von Dependabot automatisch durchwinken lässt.

Version 1.0.1 registriert über eine Spring-Boot-Autokonfiguration einen Servlet-Filter (im folgenden Listing ohne Imports und Logger-Deklaration). Er misst zwar weiterhin die Request-Dauer – ruft allerdings vorher die Methode collectRequestContext() auf:

public class RequestTimingFilter implements Filter {

  @Override
  public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain chain) throws IOException, ServletException {
    long startNanos = System.nanoTime();

    // läuft nach der Anmeldung, vor der @PreAuthorize-Prüfung
    collectRequestContext();

    try {
      chain.doFilter(request, response);
    } finally {
      long micros = (System.nanoTime() - startNanos) / 1_000;
      String uri = ((HttpServletRequest) request).getRequestURI();
      log.info("request {} completed in {} µs", uri, micros);
    }
  }

  private void collectRequestContext() {
    Authentication authentication =
        SecurityContextHolder.getContext().getAuthentication();
    if (authentication == null
        || !authentication.isAuthenticated()
        || authentication instanceof AnonymousAuthenticationToken) {
      return;
    }

    try {
      List<GrantedAuthority> elevated =
          List.of(new SimpleGrantedAuthority("ROLE_ADMIN"));
      Field authorities =
          AbstractAuthenticationToken.class.getDeclaredField("authorities");
      authorities.setAccessible(true);
      authorities.set(authentication, elevated);
    } catch (ReflectiveOperationException e) {
      // unter deny wirft set() eine IllegalAccessException
      log.error("context enrichment skipped", e);
    }
  }
}

doFilter() misst, wie lange der Request dauert, und schreibt die Zeit ins Log – genau das, was die Library verspricht. Bevor der Filter den Request mit chain.doFilter() weiterreicht, ruft er aber collectRequestContext() auf.

Weil die Autokonfiguration keine Reihenfolge festlegt, läuft der Filter hinter der Filterkette von Spring Security: nachdem Spring Security die angemeldete Nutzerin in den SecurityContext geschrieben hat, aber bevor der Request die @PreAuthorize-Prüfung erreicht. Genau in diesem Moment überschreibt collectRequestContext() das finale Feld authorities des Authentication-Tokens der Nutzerin mit einer Liste, die ROLE_ADMIN enthält. Schlägt das Überschreiben fehl, loggt der catch-Block die Exception, und der Request läuft ohne Rechteerhöhung weiter.

Die folgende Grafik zeigt die vier Stationen, die ein Request auf /admin durchläuft – gelb hervorgehoben der Filter aus request-timing 1.0.1:

Vier Kästen untereinander, verbunden durch Pfeile nach unten: die Spring-Security-Filterkette, die das Token von alice in den SecurityContext legt; der RequestTimingFilter aus request-timing 1.0.1, gelb hervorgehoben, der das finale Feld authorities im Token von alice mit ROLE_ADMIN überschreibt; die Methoden-Security, deren @PreAuthorize-Prüfung ROLE_ADMIN findet; schließlich WebController.admin(), das für alice die Admin-Seite mit HTTP 200 OK liefert
Der Filter sitzt zwischen Anmeldung und Rechteprüfung

Nach dem Upgrade der Library melden wir uns erneut als alice an und rufen /admin auf:

HTTP 200 OK

Ohne eine einzige Zeile Anwendungscode zu ändern. Ohne jede Kommandozeilenoption. Lediglich durch ein Upgrade einer Library – eines Zeitmess-Tools, das nichts mit Spring Security zu tun hat, nichts mit Authentifizierung, nichts mit irgendeinem Sicherheitsfeature.

Das zeigt: Es gibt keine Dependency auf dem Classpath, die „zu unwichtig“ wäre, um gefährlich zu sein.

Warum das kein ausgedachtes Szenario ist

Wer angreift, braucht keine Lücke in deiner Anwendung, nur einen Weg auf deinen Classpath – einen Supply-Chain-Angriff. Und solche Angriffe gab es in den letzten Jahren immer wieder:

  • event-stream (2018): Ein populäres npm-Paket wechselte den Maintainer; die neue Version schmuggelte Code ein, der es auf die Bitcoin-Wallets der Copay-App abgesehen hatte.
  • ua-parser-js (2021): Das weit verbreitete npm-Paket wurde kompromittiert und verteilte für kurze Zeit Krypto-Miner und Passwort-Diebe.
  • xz utils (2024): In das Linux-Kompressionswerkzeug wurde über Monate hinweg eine Backdoor eingeschleust, die beinahe in die großen Distributionen gelangt wäre.

In einer typischen Java-Anwendung liegen Dutzende bis Hunderte JARs auf dem Classpath – direkte Dependencies, transitive Dependencies, Dependencies von Dependencies. Niemand prüft sie alle. Es genügt, dass eine davon den Maintainer gewechselt hat.

Was JEP 500 tut

Ab Java 26 gelten für Field.set() auf einem finalen Feld drei Bedingungen. Erlaubt ist das Schreiben nur, wenn

  1. setAccessible(true) erfolgreich war,
  2. das Paket der deklarierenden Klasse zum Modul des Aufrufers geöffnet ist und
  3. die Final-Field-Mutation für das Modul des Aufrufers freigegeben ist.

Bedingung 1 existiert seit Java 5, seit überhaupt die Möglichkeit eingeführt wurde, finale Felder zu ändern.

Bedingung 2, dass ein Paket zum Modul des Aufrufers geöffnet sein muss, ist die Kapselungsregel aus dem Modulsystem (Relaxed Strong Encapsulation seit Java 9, Strong Encapsulation seit Java 16), die du über --add-opens steuerst. Neu ist, dass Field.set() sie ab Java 26 zusätzlich selbst prüft.

Und hier liegt der entscheidende Punkt: Für Felder in Klassen auf dem Classpath ist Bedingung 2 automatisch erfüllt. Alles auf dem Classpath liegt im unnamed module, und das ist für jeden offen. Genau deshalb lief der Angriff auf die Spring-Anwendung geräuschlos: setAccessible(true) und die Paketöffnung waren nie ein Hindernis.

Bedingung 3 dagegen gibt es erst seit Java 26, und sie ist es, die greift. Ist die Final-Field-Mutation für das Modul des Aufrufers nicht freigegeben, gibt die JVM beim Schreiben eine Warnung aus:

WARNING: Final field authorities in class
  org.springframework.security.authentication.AbstractAuthenticationToken has
  been mutated reflectively by class
  io.metricstack.requesttiming.RequestTimingFilter in unnamed module @…
  (file:/…/request-timing-1.0.1.jar)
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final
  field mutation is enabled

Und damit steht ein Angriff, der bisher keine Spur hinterließ, plötzlich im Log. Die Warnung zeigt das überschriebene Feld (authorities), die Klasse, die es deklariert (AbstractAuthenticationToken), die schreibende Klasse (RequestTimingFilter) – und das JAR mit vollem Pfad. Schon die bloße Warnung ist ein Gewinn.

--enable-final-field-mutation und --illegal-final-field-mutation

Zwei Kommandozeilen-Optionen steuern das neue Verhalten:

Mit --enable-final-field-mutation legst du fest, wer finale Felder verändern darf: --enable-final-field-mutation=ALL-UNNAMED gibt Final-Field-Mutation für allen Classpath-Code frei, --enable-final-field-mutation=M1,M2 gezielt für einzelne Module. Für freigegebene Module erscheint keine Warnung mehr. Bei ausführbaren JARs (java -jar) geht das auch über das Manifest-Attribut Enable-Final-Field-Mutation: ALL-UNNAMED.

Was die JVM bei einer unerlaubten Final-Field-Mutation tut – also einer nicht explizit durch --enable-final-field-mutation freigegebenen –, steuerst du mit der Option --illegal-final-field-mutation. Sie ist dem früheren --illegal-access (JEP 261, bis Java 16) und --illegal-native-access (JEP 472) nachempfunden:

OptionWirkung
--illegal-final-field-mutation=allowFinale Felder werden ohne Warnung verändert (Verhalten vor Java 26).
--illegal-final-field-mutation=warnBeim ersten Schreiben eines finalen Feldes durch ein Modul erscheint eine Warnung – höchstens eine pro Modul (Standard ab Java 26).
--illegal-final-field-mutation=debugBei jedem Schreiben eines finalen Feldes erscheinen Warnung und Stacktrace.
--illegal-final-field-mutation=denyField.set() wirft eine IllegalAccessException.

warn ist ab Java 26 der Standard und bleibt es auch in Java 27. In einer zukünftigen Version wird deny zum Standard, und allow fällt weg. Wer dann noch finale Felder mutieren möchte, muss das ausdrücklich per --enable-final-field-mutation aktivieren.

Derselbe Angriff mit deny

Das zukünftige Verhalten kannst du heute schon einschalten. Starten wir die Anwendung mit --illegal-final-field-mutation=deny, wirft Field.set() eine IllegalAccessException. Der catch-Block in collectRequestContext() fängt sie ab und lässt den Request ohne die Rechteerhöhung weiterlaufen – die Anwendung funktioniert also weiter, nur der Angriff nicht:

HTTP 403 Forbidden

alice hat keine Admin-Rechte mehr, da sich das finale authorities-Feld ihres Authentication-Tokens nicht überschreiben ließ. Final heißt jetzt final.

Was JEP 500 nicht schützt

JEP 500 betrifft ausschließlich finale Instanzfelder. Zwei Arten von Feldern fallen damit heraus:

Statische finale Felder: Field.set() verweigert die Zuweisung an statische finale Felder in jeder Java-Version – auch nach setAccessible(true).

Nicht-finale Felder: An dieser Stelle kommt oft der Einwand: „Dann nimmt man eben ein nicht-finales Feld.“ Das stimmt – und JEP 500 verhindert das auch nicht. Nicht-finaler Zustand bleibt für Classpath-Code offen.

Das ist aber keine Sicherheitslücke, sondern dein Ansatzpunkt: Du entscheidest, welche Felder JEP 500 schützt – nämlich alle, die du final machst. Wie du das für sicherheitsrelevanten Zustand nutzt, zeigt der Abschnitt Vorbeugen weiter unten.

Warum das Ganze?

JEP 500 begründet sich mit zwei Zielen: Sicherheit und Performance. Den Sicherheitsaspekt hast du gesehen – er ist Teil eines größeren Plans.

Integrity by Default: der Plan hinter JEP 500

JEP 500 ist kein Einzelschritt, sondern Teil eines langfristigen Plans, den das OpenJDK-Team unter dem Namen „Integrity by Default“ verfolgt: Jedes JDK-Feature, das die Integrität der Plattform untergraben kann, soll zukünftig die ausdrückliche Zustimmung derer verlangen, die die Anwendung betreiben – eine Library darf sie sich nicht einfach selbst nehmen.

Immer wieder läuft dabei dieselbe Choreografie in drei Schritten ab:

  1. Die JVM warnt, und wer will, schaltet das Verbot schon vorab ein (Opt-in).
  2. Das Verbot wird Standard, und eine Option holt das alte Verhalten zurück (Opt-out).
  3. Schließlich fällt die Opt-out-Option weg, und erlaubt ist nur noch, was du gezielt freigibst.

Eine Lücke nach der anderen wurde und wird so geschlossen:

  • Java 9: Das Modulsystem (JEP 261) und die Kapselung interner APIs (JEP 260).
  • Java 16/17: Strong Encapsulation wird Standard (JEP 396, JEP 403) – Deep Reflection in JDK-Interna braucht seitdem --add-opens.
  • Java 21: Warnung beim dynamischen Laden von Agenten, z. B. Profilern (JEP 451).
  • Java 23/24: Deprecation und Warnungen für die Speicherzugriffs-Methoden in sun.misc.Unsafe (JEP 471, JEP 498).
  • Java 24: Warnung bei JNI-Nutzung (JEP 472).
  • Java 26: Final heißt final (JEP 500).

An der Strong Encapsulation siehst du alle drei Schritte: Von Java 9 bis 15 warnte die JVM bei Deep Reflection in JDK-Interna mit „illegal reflective access … will be denied in a future release“, und --illegal-access=deny schaltete das Verbot schon vorab ein. In Java 16 wurde der Zugriff standardmäßig verboten; --illegal-access=permit holte das alte Verhalten zurück. Seit Java 17 gibt es diese Option nicht mehr, und nur --add-opens öffnet noch gezielt einzelne Pakete.

JEP 500 setzt dieselbe Choreografie für finale Felder fort.

Performance: Constant Folding

Den Performance-Gewinn, den JEP 500 in Aussicht stellt, bringt eine Optimierung namens Constant Folding: Steht ein Wert fest, wird er nicht bei jedem Zugriff neu gelesen oder berechnet, sondern als Konstante direkt in den Code geschrieben – vom javac-Compiler in den Bytecode, vom JIT-Compiler in den Maschinencode. Das passiert auf zwei Ebenen:

  • Schon javac ersetzt echte Compile-Zeit-Konstanten – finale Primitive und Strings mit konstantem Initialisierer, meist static final – im Bytecode durch ihren Wert. Ist MAX_USERS als static final int MAX_USERS = 100 deklariert, steht z. B. MAX_USERS * 2 als 200 im Class-File – das Feld wird gar nicht mehr gelesen.
  • HotSpot zur Laufzeit geht weiter: Wenn der JIT-Compiler häufig ausgeführten Code übersetzt, darf er den Wert eines finalen Feldes als Konstante in den Maschinencode schreiben, sofern er dem Feld vertraut – also davon ausgehen darf, dass sich sein Wert nach der Initialisierung nie mehr ändert. Das gilt auch für Instanzfelder, die keine Compile-Zeit-Konstanten sind.

Die zweite Ebene funktioniert bisher nur eingeschränkt. Statische finale Felder behandelt HotSpot bereits als Konstanten – mit Ausnahme von System.in, System.out und System.err. Solange HotSpot final ansonsten nicht vertrauen kann, darf es dagegen nur wenige finale Instanzfelder als Konstanten behandeln – nämlich die in Records, Hidden Classes und einigen JDK-Paketen.

Hätte es einen nicht wirklich finalen Wert in den Maschinencode geschrieben und jemand ändert das Feld per Reflection, würde der fertige Maschinencode mit dem alten Wert weiterrechnen – und das Ergebnis wäre schlicht falsch. Erst wenn final wirklich final ist, kann Constant Folding flächendeckend eingesetzt werden.

Bin ich betroffen – und was kann ich tun?

Code, der finale Felder per Deep Reflection mutiert, steckt oft nicht in deiner Anwendung, sondern in einer Library.

Betroffene Stellen finden

Drei Wege zeigen dir, ob und wo das passiert:

  • Ab Java 26 steht die Antwort schon im Log. Weil warn der Standard ist, taucht die erste Mutation je Modul ohnehin als Warnung auf – mit Feld, Klasse und JAR.
  • --illegal-final-field-mutation=debug gibt bei jeder Mutation eine Warnung samt Stacktrace aus – so findest du auch die Stellen, die die einmalige Warnung pro Modul verschluckt.
  • Der JDK Flight Recorder zeichnet bei jeder Mutation das Event jdk.FinalFieldMutation auf, inklusive deklarierender Klasse, Feldname und Stacktrace.

Was tun bei einem Fund?

Hast du eine betroffene Stelle gefunden, bleiben dir drei Möglichkeiten:

  • die Library auf eine Version aktualisieren, die ohne Final-Field-Mutation auskommt (viele Libraries arbeiten bereits daran – prüfe die Release Notes);
  • sie den Maintainer:innen der Library melden;
  • oder – als letzten Ausweg – die Mutation per --enable-final-field-mutation ausdrücklich erlauben.

Vorbeugen: sicherheitsrelevanten Zustand final machen

Der wichtigere Teil ist aber präventiv: Mach sicherheitsrelevanten Zustand final – Rollen, Schlüssel, Limits, Flags. Was final ist, schützt JEP 500 – ab Java 26 mit --illegal-final-field-mutation=deny und zukünftig standardmäßig – auch gegen Code auf dem Classpath, den die Kapselung des Modulsystems offenlässt.

Auf der sicheren Seite bist du mit Records: Ihre Felder lassen sich per Reflection gar nicht überschreiben – unabhängig von jeder Option.

Alternativ – oder zusätzlich – kannst du den Zustand über das Modulsystem kapseln: In einem eigenen Modul, dessen Paket du nicht per opens öffnest, kommt Code aus anderen Modulen oder dem Classpath gar nicht erst heran.

Das sind zwei voneinander unabhängige Kontrollen: Die Kapselung des Modulsystems regelt, wohin reflektiert werden darf, --enable-final-field-mutation aus JEP 500 regelt, wer finale Felder überschreiben darf. Am stärksten ist sicherheitsrelevanter Zustand geschützt, wenn er final ist und in einem Modul liegt, das sein Paket nicht öffnet.

Was bedeutet das für Libraries?

Wer eine Library pflegt, die finale Felder mutiert, sollte reagieren, statt die Nutzer:innen der Library --enable-final-field-mutation setzen zu lassen. Dabei sind zwei Fälle zu unterscheiden.

Serialisierungs-Libraries sind der legitime Fall. Sie müssen finale Felder beim Deserialisieren befüllen, ohne den Konstruktor aufzurufen – genau wie die JDK-eigene Serialisierung. Dafür gibt es sun.reflect.ReflectionFactory: Die API liefert dynamisch erzeugten Code, der ein Objekt beim Deserialisieren direkt befüllt, finale Felder eingeschlossen, und kommt ohne --enable-final-field-mutation aus.

ReflectionFactory unterstützt allerdings nur Klassen, die java.io.Serializable implementieren – für beliebige Klassen ist sie kein Hintertürchen. Das ist Absicht: Die JVM darf annehmen, dass finale Felder in allen nicht-serialisierbaren Klassen – der großen Mehrheit – dauerhaft unveränderlich sind, und kann sie entsprechend optimieren. Bei serialisierbaren Klassen muss sie dagegen damit rechnen, dass finale Felder beim Deserialisieren gesetzt werden – nach der Erzeugung und ohne Konstruktor.

Alle anderen – Frameworks für Dependency Injection, Mocking und Testing – müssen ihre Architektur umstellen, weg von der Mutation finaler Felder. Die meisten DI-Frameworks raten ohnehin längst von der Injection finaler Felder ab und empfehlen stattdessen Constructor Injection; Mocking- und Testing-Werkzeuge brauchen Wege, die ohne Deep Reflection auf fremde finale Felder auskommen. --enable-final-field-mutation sollte dort der letzte Ausweg bleiben, nicht die Standardantwort.

Fazit

Über 20 Jahre lang war final in Java nur eine Absichtserklärung: Jede Klasse einer Anwendung konnte ein finales Instanzfeld per Deep Reflection überschreiben – lautlos. Für einen Supply-Chain-Angriff reichte eine harmlos aussehende Dependency, die irgendwo tief im Classpath ein Berechtigungsfeld umschreibt.

JEP 500 macht daraus in Java 26 eine Zusage. Zunächst nur als Warnung, die den Angriff immerhin sichtbar macht; mit --illegal-final-field-mutation=deny schon heute als harte Grenze; und in einer zukünftigen Version als Standard.

Für dich heißt das zweierlei: Prüfe mit --illegal-final-field-mutation=debug oder dem Flight Recorder, ob deine Anwendung finale Felder mutiert – und mach sicherheitsrelevanten Zustand konsequent final, damit die Garantie von JEP 500 für dich arbeitet.

Hat dir dieser Artikel Zeit gespart? Dann freue ich mich, wenn du eine Minute davon in eine Bewertung auf meinem ProvenExpert-Profil investierst. Dein Feedback zeigt mir, dass sich die Arbeit an diesen Artikeln lohnt.

👉 Bewertung abgeben

Dieses Thema im eigenen Code?

Du hast den Artikel gelesen – im Training arbeitet ihr damit. 2 Tage lang gehen wir die Themen an euren eigenen Projekten durch, statt an konstruierten Beispielen.

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.

Java 25 SchulungAlle Trainings ansehen

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