Why this matters
- Most codebases skip from 8 straight to 17, so the intermediate releases arrive all at once and the useful additions are easy to miss.
- The removals — not the additions — are what actually break a migration, and they are predictable.
- Helpful NullPointerException messages alone save real debugging time, and they are on by default from 15 onwards.
The release model
Since Java 9 there is a release every six months, with a long-term-support release every two or three years. The LTS versions are 8, 11, 17, 21 and 25; those are the ones production systems target. Non-LTS releases are useful for trying preview features, not for running services on.
The Java 9–11 API additions you will use daily
// Immutable collection factories — Java 9
List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("a", "b");
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Map<String, Integer> bigger = Map.ofEntries(Map.entry("a", 1), Map.entry("b", 2));
// Defensive copies — Java 10
List<String> snapshot = List.copyOf(mutableList);
// var for local variables — Java 10
var orders = new ArrayList<Order>(); // type is inferred, still static
var entry = Map.entry("key", 1); // genuinely useful for verbose generics
for (var order : orders) { }
// Not allowed: fields, method parameters, return types, or without an initialiser
// String methods — Java 11
" text ".strip(); // Unicode-aware, unlike trim()
"".isBlank(); // true for whitespace-only
"ab".repeat(3); // "ababab"
"a\nb".lines().toList(); // a Stream<String> of lines
// Files convenience — Java 11
String content = Files.readString(path);
Files.writeString(path, content);
// Optional and Stream additions — Java 9 to 11
optional.or(() -> otherOptional);
optional.ifPresentOrElse(this::handle, this::missing);
optional.stream();
stream.takeWhile(p); stream.dropWhile(p);
Stream.iterate(1, n -> n < 100, n -> n * 2);
Stream.ofNullable(maybeNull);
Collectors.flatMapping(...); Collectors.filtering(...);
// A standard HTTP client — Java 11, replacing HttpURLConnection
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
// Single-file source execution — Java 11
// $ java Hello.java (no separate javac step)
Helpful NullPointerException messages
Before Java 14, a chained expression gave you a line number and nothing else.
// Old message
Exception in thread "main" java.lang.NullPointerException
at Example.main(Example.java:12)
order.customer().address().city().length();
Which of the three calls returned null? The old message could not say. The new one does.
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "Address.city()" because the return value of "Customer.address()" is null
at Example.main(Example.java:12)
Introduced in Java 14 behind a flag and on by default since Java 15. It costs nothing until an exception is actually thrown, because the message is computed from the bytecode at throw time.
The module system
Java 9 split the JDK itself into modules and added a way for your own code to declare the same structure.
// module-info.java at the source root
module com.example.orders {
requires com.example.core; // dependencies, checked at compile and run time
requires transitive java.sql; // consumers get java.sql too
exports com.example.orders.api; // public to everyone
exports com.example.orders.spi to com.example.plugin; // public to one module
opens com.example.orders.model; // reflection allowed — for frameworks
provides OrderHandler with DefaultOrderHandler; // a service implementation
uses PaymentGateway; // a service this module consumes
}
What modules actually give you
- Reliable configuration — missing or duplicate modules fail at startup instead of producing a
NoClassDefFoundErrorlater. - Strong encapsulation — a package not exported is genuinely inaccessible, even by reflection, unless it is
opens. - A smaller runtime —
jlinkbuilds an image containing only the modules you use, which matters for containers. - Clear dependencies — the graph is declared rather than inferred from a flat classpath.
# The migration escape hatch, when a library reflects into JDK internals
--add-opens java.base/java.lang=ALL-UNNAMED
--add-exports java.base/sun.nio.ch=ALL-UNNAMED
# Find the problems before you hit them at runtime
jdeps --jdk-internals --multi-release 17 app.jar
What was removed
This is the list that actually breaks builds.
| Aspect | Removed or deprecated | What to do instead |
|---|---|---|
| javax.xml.bind (JAXB) | Removed in Java 11 | Add the Jakarta XML Bind artifacts as dependencies |
| java.activation, java.corba | Removed in Java 11 | External dependencies, or drop CORBA entirely |
| Nashorn JavaScript engine | Removed in Java 15 | GraalVM JavaScript |
| Applets and Java Web Start | Removed | No replacement — rearchitect |
| SecurityManager | Deprecated for removal in Java 17 | OS-level and container isolation |
| finalize() | Deprecated for removal | try-with-resources, or Cleaner |
| CMS garbage collector | Removed in Java 14 | G1 (the default), or ZGC |
| Illegal reflective access | Denied by default from Java 16 | --add-opens, or upgrade the library |
javax.xml.bind (JAXB)
Removed or deprecatedRemoved in Java 11What to do insteadAdd the Jakarta XML Bind artifacts as dependenciesjava.activation, java.corba
Removed or deprecatedRemoved in Java 11What to do insteadExternal dependencies, or drop CORBA entirelyNashorn JavaScript engine
Removed or deprecatedRemoved in Java 15What to do insteadGraalVM JavaScriptApplets and Java Web Start
Removed or deprecatedRemovedWhat to do insteadNo replacement — rearchitectSecurityManager
Removed or deprecatedDeprecated for removal in Java 17What to do insteadOS-level and container isolationfinalize()
Removed or deprecatedDeprecated for removalWhat to do insteadtry-with-resources, or CleanerCMS garbage collector
Removed or deprecatedRemoved in Java 14What to do insteadG1 (the default), or ZGCIllegal reflective access
Removed or deprecatedDenied by default from Java 16What to do instead--add-opens, or upgrade the library
JAXB and reflective access into JDK internals account for the large majority of real migration failures.
Smaller things worth knowing
// Switch expressions, records, sealed classes, text blocks and pattern matching
// are covered in their own articles — they are the headline language features of 17.
// G1 is the default collector since Java 9; ZGC and Shenandoah are production-ready in 15
// Compact strings (Java 9) store Latin-1 text in one byte per char — ~25% less heap for typical apps
// Application class-data sharing (Java 12+) cuts JVM startup time with no code change
// Unified JVM logging: -Xlog:gc*:file=gc.log replaces the old ad-hoc GC flags
// jshell (Java 9) gives you a REPL for trying code without a class or main method
// Deprecated for removal now: the primitive wrapper constructors — use Integer.valueOf
A practical migration order
The sequence that fails fastest and cheapest
- Compile on 17, target 8 first —
--release 8— so you find removed APIs without changing runtime behaviour. - Run
jdeps --jdk-internalsagainst your jars to list every illegal access before it surprises you. - Upgrade build tooling and libraries next; most 8-era versions of Maven plugins, bytecode libraries and test frameworks simply do not work on 17.
- Add the removed JDK modules as dependencies — JAXB is the usual one.
- Switch the runtime to 17 with the classpath, not modules. Resist modularising at the same time.
- Only then adopt the new language features, file by file, where they clarify the code.
Common misreadings
- "Java 17 requires modules." The classpath still works, and most applications use it.
- "
varmakes Java dynamically typed." The type is inferred at compile time and fixed. - "
varworks anywhere." Local variables with an initialiser only — not fields, parameters or return types. - "
List.ofis likeArrays.asList." It is unmodifiable and rejects nulls. - "Helpful NPE messages slow things down." They are computed only when an exception is thrown.
- "Every six-month release is production-ready for you." Only LTS releases get extended updates.
- "
--add-opensis a fix." It is a temporary bridge; upgrade the library. - "Java 11 removing JAXB means XML is unsupported." JAXB moved out of the JDK into a normal dependency.
Quick recall
Everything you need if you only revisit this box.
- Six-month releases since Java 9; LTS is 8, 11, 17, 21, 25. Parse versions with
Runtime.version(), not by assuming a leading1.. - Everyday additions:
List.of/Map.of/copyOf,var,strip/isBlank/repeat/lines,Files.readString,HttpClient,Optional.or/ifPresentOrElse/stream,takeWhile/dropWhile. - Helpful NullPointerException messages name the exact null expression; default since Java 15.
- Modules give reliable configuration, strong encapsulation and
jlinkimages — but are optional for your code. The JDK's internals are encapsulated regardless. - Removals that break builds: JAXB and
java.activation(11), Nashorn (15), CMS (14), applets, andSecurityManagerdeprecated (17). - Free wins with no code change: compact strings, G1 by default, class-data sharing, unified
-Xloglogging. - Migration order: compile on 17 with
--release 8, runjdeps --jdk-internals, upgrade libraries, switch the runtime on the classpath, adopt features last.
Test yourself
Answer these before moving on — recall is what makes it stick.