What changed in Java 25

Java 25LTS

The current LTS — and a much easier first day for beginners

Two threads run through 25. Several long-running previews finally went final, and a group of changes aimed squarely at newcomers landed together, making a first Java program dramatically shorter than the ceremony people have complained about for twenty years.

Released 2025-09 · 6 features · 14 questions · 4 scenarios

Features

Compact source files and instance main methods

standardJEP 512

A source file no longer needs an explicit class declaration, and main need not be public, static, or take String[]. Final in 25 after previews starting in 21.

The problem it solved

Java's first program required a class, a public static void main, and an unused String[] parameter — five concepts before printing a line, all of which had to be taken on faith.

How you did it before

Every teaching example began with public class Main plus public static void main(String[] args), and instructors spent the first lesson explaining keywords the student could not yet use.

Journey: Preview in 21 (JEP 445), evolved through 22 and 24, final in 25.

Asked as

  • Can a Java program run without a class declaration?
  • Does main still have to be static?
  • Why did it take until Java 25 to simplify Hello World?

Scenario question

  • You are writing training material for developers new to Java, and your organisation is on JDK 17 in production but can use 25 for learning.

    Do you teach the compact form or the classic form first?

    What a good answer weighs

    Either is defensible, but the reason matters. Teaching compact first removes five upfront concepts; teaching classic first means their code matches production and every existing tutorial. The strong answer names the trade rather than picking on preference, and notes that the compact form is a source-file convenience, not a different language.

Scoped values

standardJEP 506

A way to share immutable data with a bounded dynamic scope, including across virtual threads and structured concurrency, without passing it through every method signature.

The problem it solved

ThreadLocal was the only tool for implicit context, and it leaks in pooled threads, is mutable, and its inheritance to child threads is expensive at the scale virtual threads make normal.

How you did it before

ThreadLocal with a try/finally remove(), or threading a context object through every method parameter. The first leaks in a pool, the second pollutes every signature.

Journey: Incubator in 20, preview 21 through 24, final in 25.

Asked as

  • What problem do scoped values solve that ThreadLocal does not?
  • How does ThreadLocal leak memory in a thread pool?
  • Why do virtual threads make ThreadLocal more of a problem?

Scenario question

  • A request-scoped tenant id needs to reach code six layers deep. The service is moving to virtual threads and may create hundreds of thousands of them.

    ThreadLocal, a parameter, or a scoped value?

    What a good answer weighs

    A scoped value, and the reasoning is what is being tested: the value is immutable and its lifetime is bounded by the scope, so there is nothing to remove in a finally and nothing to leak. ThreadLocal at that thread count also multiplies per-thread storage. Passing a parameter is still the most explicit option and worth naming as the honest alternative.

Module import declarations

standardJEP 511

import module java.base; makes every package exported by a module available, rather than importing packages one at a time.

The problem it solved

Small programs and scripts accumulated a wall of imports before any real code, which is friction for learners and for single-file programs.

How you did it before

Explicit per-package imports, or a wildcard import per package, with the IDE managing the list.

Asked as

  • What does import module do, and how is it different from a wildcard import?
  • What are the risks of importing a whole module?

Flexible constructor bodies

standardJEP 513

Statements may appear before super(...) or this(...), as long as they do not reference the instance under construction.

The problem it solved

Argument validation had to happen inside the super() call itself, often via a contrived static helper, because nothing could precede it.

How you did it before

A private static validation method invoked inside the super(...) arguments, or validation after construction — by which point an invalid object already exists.

Asked as

  • Why could nothing come before super() in a constructor?
  • How did you validate constructor arguments before Java 25?

Scenario question

  • A subclass constructor must reject a negative amount before the superclass constructor runs, because the superclass registers the object in a static collection.

    How would you have solved this before 25, and now?

    What a good answer weighs

    Before: a static helper called within the super(...) argument list, which works but reads badly and cannot be reused for multiple checks. Now: a plain validation statement before super(). The good answer also spots the real smell — a superclass constructor with a side effect that publishes a partially constructed object.

Compact object headers

standardJEP 519

Shrinks the object header on 64-bit HotSpot, reducing heap use and improving locality for allocation-heavy workloads.

The problem it solved

Every object carried a header sized for capabilities most objects never use, which on heaps full of small objects is a large fraction of the total footprint.

How you did it before

The only levers were allocating fewer objects, or relying on compressed oops, which shrank references rather than headers.

Asked as

  • What is in an object header, and why does its size matter?
  • Which workloads benefit most from smaller object headers?

Structured concurrency

previewJEP 505

Treats a group of concurrent subtasks as one unit with a defined lifetime, so failure and cancellation propagate predictably. Still preview in 25, at its fifth round.

The problem it solved

An ExecutorService gives no relationship between tasks: one subtask failing does not cancel its siblings, and a leaked task can outlive the request that started it.

How you did it before

ExecutorService with manual future tracking, explicit cancellation on failure, and hand-written timeout handling — correct only if you never forgot a path.

Journey: Incubator in 19-20, preview from 21 (JEP 453) through the fifth preview in 25.

Asked as

  • What problem does structured concurrency solve that ExecutorService does not?
  • Why has structured concurrency been in preview for so many releases?

Scenario question

  • A request fans out to three services and needs all three to succeed. Today one failure leaves the other two calls running and the request hanging until timeout.

    How do you fix it, and what would structured concurrency change?

    What a good answer weighs

    Today: invokeAll or explicit cancellation of remaining futures in a failure handler, plus a timeout on each. With structured concurrency the scope owns the subtasks, so a failure cancels the siblings and nothing outlives the scope. The senior answer notes it is still preview, so production use needs --enable-preview and a deliberate decision.

Other versions