Java 23, Decoded: What Changed and Why It Matters

~
~
Published on
Authors
java-23-banner

Java 23 reached General Availability on September 17, 2024. It’s a focused release: a handful of language previews, library/tooling improvements, and a meaningful runtime default change. The headline features are:

  • Primitive type patterns (JEP 455, Preview)
  • Module import declarations (JEP 476, Preview)
  • Structured concurrency (JEP 480, 3rd Preview)
  • Scoped values (JEP 481, 3rd Preview)
  • Stream gatherers (JEP 473, 2nd Preview)
  • Class-File API (JEP 466, 2nd Preview)
  • Markdown doc comments (JEP 467)
  • Vector API (JEP 469, 8th Incubator)
  • ZGC generational mode by default (JEP 474)

  • junior dev: Java 23 makes “getting started” smoother and modern concurrency more approachable.

  • senior dev: it gives you more expressive language tools, better concurrency structure, and a GC default that targets predictable performance.

What’s new at a glance

Language & ergonomics (previews)

  • JEP 455: Primitive types in patterns, instanceof, and switch (Preview)
  • JEP 476: Module import declarations (Preview)
  • JEP 482: Flexible constructor bodies (Second Preview)
  • JEP 477: Implicitly declared classes & instance main methods (Third Preview)
  • JEP 480: Structured concurrency (Third Preview)
  • JEP 481: Scoped values (Third Preview)

Libraries & tooling

  • JEP 466: Class-File API (Second Preview)
  • JEP 467: Markdown documentation comments
  • JEP 473: Stream gatherers (Second Preview)
  • JEP 469: Vector API (8th Incubator)

Runtime & performance

  • JEP 474: ZGC generational mode enabled by default (improves efficiency for common object lifecycles)

Housekeeping / gotchas

  • JEP 471: Deprecate sun.misc.Unsafe memory-access methods for removal
  • String Templates are not included (withdrawn)
  • javac annotation processing disabled by default (opt in if you rely on processors)
  • Legacy thread control methods like Thread.suspend/resume and ThreadGroup.stop were removed

Why this release matters (quick takes)

For junior developers

  • Cleaner on-ramps: less ceremony for learning and quick prototypes (implicit classes & instance main).
  • Better examples & docs: Markdown in Javadoc makes learning materials more readable.
  • Safer concurrency mental model: structured concurrency + scoped values pushes you toward more maintainable patterns early.

For senior developers

  • Less boilerplate, fewer foot-guns: primitive patterns and constructor flexibility remove common “just-Java-things” friction.
  • Concurrency that reads like intent: structured concurrency gives you “a unit of work” instead of thread soup; scoped values reduce accidental context leaks.
  • Predictable performance: ZGC generational by default is a practical nudge toward better runtime behavior out of the box for ZGC users.

Feature deep dives (snackable, with examples)

Notes on status:

  • Preview / Incubator features require opting in.
  • You can try them safely in a branch, CI lane, or sandbox module and provide feedback upstream.

JEP 455 — Primitive Types in Patterns / instanceof / switch (Preview)

Pattern matching has been steadily expanding; Java 23 brings primitives into the story. The payoff is fewer manual guards and clearer intent when handling numeric-heavy logic (parsing, protocols, analytics).

Before (classic):

Object o = 42;
if (o instanceof Integer) {
  int x = (Integer) o;
  if (x > 0) { /* ... */ }
}

After (directionally):

Object o = 42;
// In Java 23 preview, patterns broaden to better express primitive cases in instanceof/switch.
switch (o) {
  // ... pattern cases that reduce manual casting/guard boilerplate
  default -> {}
}

Why it matters: pattern matching keeps trending toward “write the shape of the data, not the plumbing”.

JEP 476 — Module Import Declarations (Preview)

Ever used modular libraries and wished you could import “the module” rather than hunting for the right packages? That’s the vibe here: import all packages exported by a module with one declaration.

Conceptually:

// new form (preview) that can reduce noisy imports:
import module java.base;
// or a third-party module
import module com.example.lib;

Why it matters: fewer import lines, clearer dependency intent—especially when teaching, prototyping, or consuming large modular APIs.

JEP 482 — Flexible Constructor Bodies (Second Preview)

Java constructors can be awkward when you want to do safe initialization but are constrained by this(...)/super(...) ordering. This preview relaxes that in controlled ways, reducing “constructor gymnastics.”

What you get: more robust initialization patterns without resorting to factory methods for everything.

JEP 477 — Implicitly Declared Classes & Instance Main Methods (Third Preview)

This is the “just write Java” experience: fewer boilerplate hurdles for small programs and learning examples, while still being Java (not a separate language).

Old hello world:

public class Hello {
  public static void main(String[] args) {
    System.out.println("Hello");
  }
}

New direction (preview):

void main() {
  System.out.println("Hello");
}

Why it matters: better onboarding and faster experimentation.

JEP 480 — Structured Concurrency (Third Preview)

Structured concurrency treats concurrent tasks as a single unit of work: start together, cancel together, fail together, observe together. This makes code easier to reason about and test.

High-level example shape:

// Pseudocode-ish to illustrate the idea:
try (var scope = /* structured task scope */) {
  var user = scope.fork(() -> loadUser(id));
  var orders = scope.fork(() -> loadOrders(id));
  scope.join();            // wait for tasks
  scope.throwIfFailed();   // propagate errors
  return combine(user.get(), orders.get());
}

Why it matters: fewer dangling threads, clearer cancellation rules, better observability.

JEP 481 — Scoped Values (Third Preview)

Scoped values are a safer alternative to thread-locals for immutable, inheritable context (request IDs, tracing tokens, locale, etc.). They help avoid the “context leaks across threads” class of bugs.

Why it matters: in concurrent systems, “where did this context come from?” is a real production question. Scoped values are designed for that reality.

JEP 473 — Stream Gatherers (Second Preview)

Stream gatherers extend the Stream API with customizable aggregation steps—think “write my own intermediate operation” without contorting collectors.

Why it matters: more expressive stream pipelines for transformations that don’t fit cleanly into today’s built-in ops.

JEP 466 — Class-File API (Second Preview)

A standard API to parse and transform class files—useful for tooling, agents, and frameworks that currently lean on internal APIs or external libraries.

Why it matters: less reliance on internals, more stable tooling ecosystems.

JEP 467 — Markdown Documentation Comments

Javadoc comments can be written in Markdown, which makes docs easier to author and more readable in many workflows.

Example:

/**
 * # PaymentService
 *
 * - Retries transient failures
 * - Emits metrics
 *
 * ```java
 * service.charge(userId, amount);
 * ```
 */
class PaymentService { /* ... */ }

JEP 469 — Vector API (8th Incubator)

Portable SIMD: write data-parallel code that can map to vector instructions on supported hardware. This incubator keeps iterating toward a stable API.

Why it matters: performance-critical paths (image/audio processing, ML-ish math, analytics) can speed up without going full native.

JEP 474 — ZGC Generational Mode by Default

Java 23 changes ZGC’s default to generational mode, aimed at improving garbage collection efficiency under typical allocation patterns.

Why it matters: many apps allocate lots of short-lived objects; generational collectors are optimized for that reality.

Compatibility notes & surprises (don’t skip)

String Templates are withdrawn

String Templates were previewed earlier (JDK 21/22) and intended to re-preview, but were withdrawn from Java 23 after extensive feedback; there wasn’t consensus on a better design yet.

Takeaway: don’t ship production code that depends on preview features without a plan for churn.

javac annotation processing is disabled by default

Starting in Java 23, annotation processing runs only with explicit configuration or explicit command-line request.

If your build uses Lombok, MapStruct, AutoValue, Dagger, etc.:

  • Make sure your build tool explicitly enables annotation processing in CI.
  • Verify IDE settings match CI to avoid “works locally, fails in pipeline” drama.

Legacy thread control methods removed

If you still have code (or dependencies) using legacy thread stop/suspend APIs, Java 23 will break that.

Takeaway: modernize toward structured concurrency, executors, and safe cancellation patterns.

Should you upgrade?

Here’s a pragmatic take:

  • If you’re on ZGC and performance-sensitive: yes, Java 23 is worth a look because of the generational default.
  • If you build tools / bytecode agents / frameworks: yes—Class-File API and Markdown Javadoc are immediate wins.
  • If you’re a typical application team: upgrade if you can, but treat previews as “try + learn,” not “bet the codebase.” Run a CI lane with Java 23, measure, and adopt what’s stable.

How to try Java 23 previews safely (quick recipe)

Compile/run with preview features

javac --release 23 --enable-preview YourFile.java
java --enable-preview YourMain

Maven (example idea)

  • Configure the compiler plugin to use --enable-preview
  • Add a CI job that compiles and runs tests with Java 23 (preview flags enabled)

Team rule of thumb

  • Use previews in a dedicated module or behind an abstraction boundary.
  • Keep a “remove preview usage” checklist so upgrades don’t become archaeology.

Final takeaway

Java is steadily making the good path the easy path—less ceremony, better defaults, and safer concurrency primitives. The move is to invest in idioms, not just APIs: the language and runtime are evolving together, and Java 23 is a very clear step in that direction.