🚀 Modern Java 17 → 25 · Advanced

Migrating legacy code in Java

Java 8 → 25 upgrade benefits and removed APIs (finalization, Security Manager).

🧩 The mysteryA Java 8 app boots on Java 21 and dies instantly: NoClassDefFoundError: javax/xml/bind/JAXBContext. Upgrading is worth it, as long as you know what was removed along the way.

What you gain

Moving from Java 8 to 25 brings records, pattern matching, text blocks, var, virtual threads, better garbage collectors and big performance gains, often just from upgrading. Old idioms get lighter: anonymous Runnable → lambda, Date/Calendar → java.time, hand-written POJOs → records, huge I/O pools → virtual threads.

Gone: the Java EE modules

Java 11 (JEP 320) removed the Java EE and CORBA modules from the JDK, including JAXB and JAX-WS. They live on as ordinary libraries, today under jakarta.xml.bind and friends. Code that compiled fine on Java 8 now needs them added as dependencies.

🤔 Think first

The missing class

After upgrading, the app fails with NoClassDefFoundError: javax/xml/bind/JAXBContext. Will --enable-preview, an --add-opens flag or a different GC fix it?

Think about it, then reveal the answer

None of them. The class isn't in the JDK at all anymore: it isn't hidden, it's removed. Add JAXB as a library dependency and the error disappears.

Locked doors

Since Java 17, JDK internals are strongly encapsulated: reflective access to them fails unless you explicitly --add-opens. Find them with jdeps --jdk-internals. Since Java 24 (JEP 486) the Security Manager is permanently disabled: enabling it at startup is an error, and System.setSecurityManager throws UnsupportedOperationException.

Replacing finalize()

✗ Finalizer
class NativeFile {
    protected void finalize() {
        closeHandle(); // someday, maybe
    }
}

Finalization is unpredictable, slow, and deprecated for removal. Calling System.gc() or runFinalization() more often doesn't fix it.

✓ AutoCloseable
class NativeFile implements AutoCloseable {
    public void close() { closeHandle(); }
}
try (var f = new NativeFile()) {
    f.read();
}

try-with-resources releases the handle deterministically at the end of the block. Add a Cleaner as a safety net if needed.

Pick an LTS

Most teams upgrade from one LTS release to the next, like 17, 21 or 25. Non-LTS releases are typically supported only until the next release six months later, while LTS releases receive updates for years, so upgrades can follow a predictable schedule.

💼 In the real world

A migration playbook

1. Run jdeps --jdk-internals and update build plugins plus bytecode-heavy libraries (mocking, Lombok, agents). 2. Add removed modules like JAXB as dependencies. 3. Get the tests green on the new JDK. 4. Only then modernize: records, switches, virtual threads, one module at a time.

Key takeaways

  1. Run jdeps --jdk-internals to find uses of JDK internals
  2. JAXB / JAX-WS left the JDK in 11 — add them as libraries
  3. Replace finalize() with try-with-resources or a Cleaner
  4. The Security Manager can't be enabled since Java 24
🤯 Did you know?

The Security Manager dates back to Java 1.0 and applets. It survived almost 30 years before Java 24 disabled it for good.

Practice questions

After moving from Java 8 to Java 21, an app fails with NoClassDefFoundError: javax/xml/bind/JAXBContext. What's the fix?

  1. Add JAXB as a library dependency — it was removed from the JDK in Java 11
  2. Run with --enable-preview
  3. Add --add-opens java.base/java.lang=ALL-UNNAMED
  4. Switch to a different garbage collector
Check your answer

Add JAXB as a library dependency — it was removed from the JDK in Java 11. The Java EE modules, including JAXB, were removed from the JDK in Java 11 (JEP 320). They now ship as normal libraries (today under jakarta.xml.bind).

A legacy class releases a native file handle in finalize(). What's the modern replacement?

  1. Implement AutoCloseable and use try-with-resources, with a Cleaner as a safety net
  2. Call System.runFinalization() more often
  3. Keep finalize() but make it synchronized
  4. Call System.gc() after using the object
Check your answer

Implement AutoCloseable and use try-with-resources, with a Cleaner as a safety net. Finalization is unpredictable, slow and deprecated for removal. try-with-resources releases the handle deterministically at the end of the block.

Next world: from language features to design. SOLID principles and the classic patterns, from Singleton to Strategy, written the modern-Java way.