Java · Java 17 Features (9 to 17)

Java 17: New APIs, Modules and the Platform

The collection factories and their strict rules, views versus copies, the String, Stream and Optional additions, HttpClient - and the module system, strong encapsulation and removed modules that make 8 to 17 upgrades hard.

  • 5 min read
  • 12 practice questions
  • 1 graded problem
On this page
  1. Description
  2. Collection factories
  3. Read-only views versus copies
  4. The other small additions
  5. Modules and strong encapsulation
  6. What else changed under the hood
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

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:

  1. Collection factories: List.of, Set.of, Map.of and their rules.
  2. Read-only views versus copies.
  3. The other small additions: strings, streams, files, HTTP.
  4. Modules and strong encapsulation: why old libraries break.
  5. 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, put and set throw UnsupportedOperationException.
  • No nulls, not even in questions. List.of("a", null) throws a NullPointerException, and so does List.of("a").contains(null), where an ArrayList would just return false.
  • No duplicates. Set.of("a", "a") and Map.of("Austin", 1, "Austin", 2) throw IllegalArgumentException, where a HashSet would quietly ignore the second.
  • No promised order for Set.of and Map.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. Use LinkedHashSet or TreeMap when 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

APISinceWhat to know
String.isBlank(), strip(), lines(), repeat(n)11strip() removes all Unicode white space; trim() only removes characters up to U+0020, so it misses spaces like the em space
Stream.takeWhile / dropWhile9stop at the first element that fails, unlike filter
Optional.ifPresentOrElse, or, isEmpty()9, 11
Predicate.not(String::isBlank)11negates a method reference
Files.readString, Files.writeString11read or write a whole text file in one call
HttpClient11HTTP/2, synchronous send or asynchronous sendAsync; replaces HttpURLConnection
Stream.toList()16an 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 of java.* 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 with InaccessibleObjectException: 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 with NoClassDefFoundError: javax/xml/bind/... until you add them back as ordinary dependencies (now under the jakarta.* 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

MistakeWhat happensDo this instead
contains(null) on a List.of listNullPointerExceptiondon't probe for null, or use an ArrayList
Duplicate keys in Map.ofIllegalArgumentExceptionbuild with a loop or toMap with a merge function
Relying on Set.of/Map.of orderfails some runsLinkedHashSet, TreeMap, or sort
Returning an unmodifiable viewthe caller sees later changesList.copyOf
trim() on user inputUnicode spaces survivestrip()
takeWhile on unsorted datastops at the first failurefilter
Upgrading from 8 without checking dependenciesstartup failuresupgrade 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.of and Map.of are unmodifiable, reject null and duplicates, and have no promised order.
  • A view follows its source; List.copyOf takes a snapshot.
  • Prefer strip() over trim(); takeWhile stops 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.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Collection factories

List.of("a", "b");  Set.of("x", "y");  Map.of("k", 1);  Map.ofEntries(Map.entry("k", 1));
List.copyOf(source);                       // snapshot (10)
Collections.unmodifiableList(source);      // live read-only view

Unmodifiable · no null (even contains(null) throws) · no duplicates (IllegalArgumentException) · Set.of/Map.of order changes per run.

Small APIs worth knowing

APISince
isBlank, strip (Unicode-aware, unlike trim), lines, repeat11
takeWhile/dropWhile, Stream.ofNullable, bounded iterate9
Optional.or, ifPresentOrElse, stream (9), isEmpty (11)9-11
Predicate.not11
Files.readString/writeString, HttpClient11
Collectors.teeing12
Stream.toList16
jshell (9), java Main.java (11)

Upgrading from 8

  • InaccessibleObjectException: a library reflects into JDK internals (locked in 17). Upgrade it; --add-opens only as a stopgap.
  • NoClassDefFoundError: javax/xml/bind/...: Java EE modules removed in 11; add jakarta.* dependencies.
  • Removed: CMS (14), Nashorn (15), many JVM flags. Security Manager deprecated (17).

The traps

  • contains(null) on factory collections; duplicate keys in Map.of; relying on Set.of order.
  • Returning a view when a snapshot was meant.
  • trim() on Unicode input; takeWhile on unsorted data.

Staff-level points, one line each

  • module-info: requires (+ transitive), exports (public types), opens (deep reflection), uses/provides.
  • Readability (requires) and accessibility (exports/opens) are separate checks; classpath code is the unnamed module.
  • Split packages are fatal across named modules; automatic modules export everything.
  • --add-opens/--add-exports/--add-reads/--add-modules, manifest Add-Opens; jdeps --jdk-internals; jlink for small runtimes.
  • In containers: max heap = 25% of the limit (2 GB → 512 MB, measured); under 2 CPUs or ~1792 MB, the default GC is Serial, not G1 (measured). Set -XX:MaxRAMPercentage and the GC explicitly.
  • availableProcessors() follows the CPU limit, sizing the common pool and GC threads.
  • List12/ListN/SetN/MapN: compact, open addressing, null-hostile by design; per-JVM salt randomizes order.
  • HttpClient: one shared instance (pooling, HTTP/2), connect and request timeouts, sendAsync → CompletableFuture.

Say it in an interview

  • "Most of a Java 8 to 17 upgrade is upgrading the libraries that reflect into JDK internals."
  • "In a 2 GB container the default heap is only 512 MB, so I set MaxRAMPercentage explicitly."
  • "I'd return List.copyOf so callers can't see our later changes."

Practice

This skill comes with 12 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Java 17: New APIs, Modules and the Platform free

Clean Up a Contact Import

EasyFreeCoding problemJava

A CRM in Toronto imports contacts that people paste from spreadsheets and old systems: one email address per line, with stray spaces (some of them Unicode spaces copied from documents), empty lines, comment lines starting with #, and the same person in different capitals.

Practice Java 17: New APIs, Modules and the Platform free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime