On this page
Description
Between Java 9 and 17, the JDK gained dozens of small API additions that you now meet in almost every codebase: List.of, String.strip, Files.readString, a modern HttpClient. It also changed underneath: the JDK was split into modules, its internals were locked away, and some old parts (like JAXB) were removed.
Both halves matter for backend work. The small APIs make everyday code shorter, but they have rules that surprise people (a list that throws when you ask whether it contains null). And the platform changes are what actually makes upgrading an old Java 8 service hard: it compiles fine, then fails at startup.
Here's the path, from easiest to most technical:
- Collection factories:
List.of,Set.of,Map.ofand their rules. - Read-only views versus copies.
- The other small additions: strings, streams, files, HTTP.
- Modules and strong encapsulation: why old libraries break.
- What else changed under the hood.
Collection factories
Java 9 added short ways to create small collections:
List<String> cities = List.of("Austin", "Dublin");
Set<String> countries = Set.of("US", "IE");
Map<String, Integer> stock = Map.of("A1", 3, "B2", 0); // up to 10 pairs; Map.ofEntries for more
They return unmodifiable collections, with four rules:
- No changes.
add,remove,putandsetthrowUnsupportedOperationException. - No
nulls, not even in questions.List.of("a", null)throws aNullPointerException, and so doesList.of("a").contains(null), where anArrayListwould just returnfalse. - No duplicates.
Set.of("a", "a")andMap.of("Austin", 1, "Austin", 2)throwIllegalArgumentException, where aHashSetwould quietly ignore the second. - No promised order for
Set.ofandMap.of. The iteration order is deliberately shuffled differently on each JVM run, so code or tests that depend on it fail some of the time. UseLinkedHashSetorTreeMapwhen order matters.
Read-only views versus copies
There are two ways to hand out a list that callers can't change, and they behave differently:
List<String> source = new ArrayList<>(List.of("x"));
List<String> view = Collections.unmodifiableList(source); // a read-only window onto source
List<String> copy = List.copyOf(source); // Java 10: an independent snapshot
source.add("y");
// view is [x, y]; copy is still [x]
A view follows its source; a copy doesn't. When a getter must not show the caller later changes, return List.copyOf(...).
The other small additions
| API | Since | What to know |
|---|---|---|
String.isBlank(), strip(), lines(), repeat(n) | 11 | strip() removes all Unicode white space; trim() only removes characters up to U+0020, so it misses spaces like the em space |
Stream.takeWhile / dropWhile | 9 | stop at the first element that fails, unlike filter |
Optional.ifPresentOrElse, or, isEmpty() | 9, 11 | |
Predicate.not(String::isBlank) | 11 | negates a method reference |
Files.readString, Files.writeString | 11 | read or write a whole text file in one call |
HttpClient | 11 | HTTP/2, synchronous send or asynchronous sendAsync; replaces HttpURLConnection |
Stream.toList() | 16 | an unmodifiable list |
Two tools are worth knowing too: jshell (Java 9), an interactive prompt for trying out code, and java Main.java (Java 11), which compiles and runs a single source file in one step.
Modules and strong encapsulation
Java 9 introduced modules: a JAR can declare, in a module-info.java file, which other modules it needs and which of its packages others may use. Everything else is hidden, even public classes. Most applications still run on the old classpath without declaring modules, but the JDK itself became modular, and that's what upgrades run into:
- The JDK's internals are locked. Internal packages (
sun.misc,jdk.internal.*) and the private fields ofjava.*classes can no longer be reached by reflection. Java 9 to 15 allowed it with a warning, Java 16 refused by default, and Java 17 removed the switch that turned the warning back on. Old libraries that do this fail withInaccessibleObjectException: Unable to make field ... accessible. The fix is almost always to upgrade the library; as a temporary workaround, the JVM can be started with--add-opens java.base/java.lang=ALL-UNNAMED. - Some parts were removed. Java 11 removed the Java EE modules, including JAXB (
javax.xml.bind) and JAX-WS. Code using them fails withNoClassDefFoundError: javax/xml/bind/...until you add them back as ordinary dependencies (now under thejakarta.*names).
A typical Java 8 to 17 upgrade therefore compiles fine, then fails at startup with exactly those two errors. Most of the work is upgrading dependencies, not changing your own code.
What else changed under the hood
- Garbage collectors: G1 became the default in Java 9; ZGC and Shenandoah, which keep pauses very short, became production-ready in Java 15; the old CMS collector was removed in Java 14.
- Removed: the Nashorn JavaScript engine (Java 15). The Security Manager was deprecated for removal in Java 17.
- Containers: since Java 10, the JVM reads the container's memory and CPU limits instead of the whole machine's, so its defaults fit the container (the deep dive has the numbers).
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
contains(null) on a List.of list | NullPointerException | don't probe for null, or use an ArrayList |
Duplicate keys in Map.of | IllegalArgumentException | build with a loop or toMap with a merge function |
Relying on Set.of/Map.of order | fails some runs | LinkedHashSet, TreeMap, or sort |
| Returning an unmodifiable view | the caller sees later changes | List.copyOf |
trim() on user input | Unicode spaces survive | strip() |
takeWhile on unsorted data | stops at the first failure | filter |
| Upgrading from 8 without checking dependencies | startup failures | upgrade libraries; --add-opens only as a stopgap |
Check yourself
Try to answer before opening each one. Recalling beats re-reading.
1. What does List.of("a").contains(null) do, and how is that different from an ArrayList?
Show answer
It throws a NullPointerException; an ArrayList returns false. The factory collections reject null everywhere, even in queries.
2. After source.add("y"), what do Collections.unmodifiableList(source) and List.copyOf(source) (both made before the add) show?
Show answer
The view shows the new element, because it's a read-only window onto source. The copy doesn't, because it's a snapshot.
3. User input has an em space at each end. Why does trim() leave it, while strip() removes it?
Show answer
trim() only removes characters up to U+0020 (the ordinary space and control characters). strip() removes everything Unicode classifies as white space, including the em space.
4. A service upgraded from Java 8 to 17 fails at startup with InaccessibleObjectException. What happened?
Show answer
A library uses reflection to reach inside the JDK's internals, which Java 17 locks completely. Upgrade the library; as a temporary workaround, open the package with --add-opens.
Key takeaways
List.of,Set.ofandMap.ofare unmodifiable, rejectnulland duplicates, and have no promised order.- A view follows its source;
List.copyOftakes a snapshot. - Prefer
strip()overtrim();takeWhilestops at the first failure. - Java 17 locks the JDK's internals and Java 11 removed Java EE modules: those, not syntax, are the real work of upgrading from Java 8.