IntelliJ IDEA
IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin
Java 25 LTS and IntelliJ IDEA
The Java release cadence means we get a new Java version every six months. Java 25 was released on September 16, 2025. At JetBrains, we are committed to supporting new technologies in IntelliJ IDEA and adding useful enhancements for both stable and preview features. In this blog post, we will give you an overview of some changes to the Java language and how they are supported in IntelliJ IDEA. This post is limited to stable features only. Preview features will be covered separately in dedicated blog posts on relevant topics.
Java 25 includes several changes to the language that make Java easier to use. Features like compact source files and instance main methods, as well as module import declarations, make it easier to get started with Java, both for students and when creating small projects like prototypes or hobby projects. Flexible constructor bodies allow more flexibility in constructors, giving you the option to calculate or validate data before calling the constructor of the super class. Scoped values are a new model for thread-local variables, adapted to virtual threads. They will be more useful with structured concurrency, which is currently still in preview.
Apart from changes to the language itself, there are improvements to both performance and performance insights. Compact object headers reduce memory footprint and improve cache efficiency. Ahead-of-time method profiling lets the JVM warm up more quickly by using execution data from prior runs, improving startup performance. Improvements to garbage collection (GC) like generational Shenandoah, plus better class-loading and linking optimizations, contribute to noticeably smoother server-style workloads.
As Java 25 is an LTS (long-term support) release, many people will be migrating to this version from Java 21, 17, 11, or even earlier. If you are coming to Java 25 from Java 21, have a look at the section describing the most relevant changes since Java 21.
Before diving into the new features, let’s set up IntelliJ IDEA to use Java 25.
Using Java 25 in IntelliJ IDEA (setup)
To use Java 25, you will need to download the JDK. You can do so from inside IntelliJ IDEA or by using tools like SDKMAN! To download a JDK from IntelliJ IDEA, open the Project Structure, go to the tab Project Settings | Project, open the drop-down menu in the SDK field, and select Download JDK.

In the Download JDK popup that opens, set Version to 25, and in the Vendor field, select the vendor you want to use.
You can also download Early Access (EA) versions of the JDK from inside IntelliJ IDEA, for example, when the next release becomes available or if you’d like to try Valhalla (which is based on Java 23). IntelliJ IDEA will warn you that these are not intended for production use.

Next, you need to configure IntelliJ IDEA to use the right language level. To use Java 25 stable features, which is recommended for production code, set Language level to 25 – Compact source files, module imports.

If you want to try out preview features, set Language level to 25 (Preview) – Primitive Types in Patterns, etc.

New stable features in Java 25
Let’s take a look at some of the features Java 25 introduces and how IntelliJ IDEA can help you use them.
Compact Source Files and Instance Main Methods (JEP 512)
Java has been working on the so-called “on-ramp”, making the language easier to use. Compact source files and instance main methods are part of that effort. It is now possible for beginners to start writing code without needing to learn about language concepts that they won’t need until they start writing larger programs. For experienced programmers, this feature can help them quickly prototype ideas without needing a lot of boilerplate code. Code can be evolved and expanded as skills and applications grow.
To quickly see the difference, let’s look at a classic example: `HelloWorld
`. We have probably all written a HelloWorld example when we first started, possibly in a language other than Java or English. The classic `HelloWorld.java
` looks like this:
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }
To write this code, you had to declare a class and a lengthy main method, including concepts like `public
` and `static
` that are not relevant to beginners. Let’s compare this to the `HelloWorld
` example using new features from compact source files and instance main methods.
When creating a new Java class via New | Java Class, in the New Java Class popup, select the Compact source file option. Note that this compact source file is created in the root directory of your project, even if you create it from another package. IntelliJ IDEA automatically adds an instance main method – `void main()
` – to the file. Next, you can add a method to print “Hello, World!”. You can now use `IO.println()
` as a convenience method without needing to understand what `System.out
` means and without even needing to add a static import. If you do want to add a static import for `java.lang.IO
`, IntelliJ IDEA offers a quick-fix to do so.
Note that different variations of the main method are now possible, as described here.
IntelliJ IDEA has some new live templates to add a main method to an implicit class, either with or without arguments: `main
`, `maina
`, `psvm
`, and `psvma
`. Using the `psvm
` or `main
` live templates inside a compact source file will add the new main method, while they will continue to add the classic main method inside a class, as you can see in the preview.


Our new version of `HelloWorld
` now looks like this:
void main() { IO.println("Hello, World!"); }
Compare this code to the original example. It is much shorter, contains less boilerplate, and is limited to only the things we need: a main method and a call to print a line with the provided “Hello, World!” `String
`.
As beginners often need to interact with the console, a convenient `readln()
` method was also added. This is an overloaded method which can take a `String
` argument that is printed to the console before reading the input. Let’s expand our previous example to read a name from the console. To help you use these new convenience methods, IntelliJ IDEA introduces two new live templates: `iop
` for `println()
` and `ior
` for `readln()
`.
Note that these changes to the language are also taken into account when creating new projects. When you create a new Java project, set Build system to IntelliJ, and select Add sample code, a compact source file with a `void main()
` method will be added.
Prototyping and teaching
While extremely useful for students and teachers, this feature does not just benefit beginners. It also allows experienced developers to quickly try out ideas or create a prototype.
When you create a new Java project with Maven or Gradle as the selected build system, the generated source code will include a regular class, but with `IO.println()
` instead of `System.out.println()
`. If you are using Maven or Gradle, you’re likely working on something bigger, with actual classes instead of compact source files. Another reason to use classes is that a compact source file needs to be in the default package, and frameworks generally don’t support this.
To quickly create a prototype, you can create a Java compact file from the `src/main/java
` directory in the Project tool window. IntelliJ IDEA will provide a default name for the file, so your thought process is not disrupted when you want to quickly try something out.
When prototyping, learning, or teaching, you can gradually expand your code to include features you might need when writing code that is part of a larger project, or when introducing new concepts to your students. You can convert an implicit class to a regular class using the Convert an implicitly declared class of a compact source file into a regular class quick-fix.
Should you prefer to use an implicit class at any point, the reverse is also possible. Similarly, there are quick-fixes to convert `IO.println()
` to `System.out.println()
`, and vice versa. These are probably not things you would do as part of your daily work. But you might use features like this for coding challenges, like Advent of Code, or other fun side projects.
The new `void main()
` method does not need `String[] args
`. However, should you decide to use the `args
` in your code, IntelliJ IDEA will help you by adding them to the method, as you can see below. If you like this kind of completion, please let us know in the comments what you are currently missing.
With this feature, we finally have a separation in Java between prototypes and other small projects, and enterprise applications. IntelliJ IDEA supports both small and large applications.
We have already covered this feature in a previous Java 24 and IntelliJ IDEA post, when it was still in preview. Please have a look at that post to learn about additional support for this feature in IntelliJ IDEA. For a more in-depth explanation of this specific feature, see Mala Gupta’s previous post Java 24: ‘HelloWorld’ and ‘main()’ meet minimalistic. There are examples of practical use cases on when and how to use it to create small programs and prototypes in Java 24: Build Games, Prototypes, Utilities, and More – With Less Boilerplate. Note that (among other things) the following was changed in Java 25: Compact source files were previously called simple source files, and you now need to use the qualified name `IO.println()
` or use an import statement.
Module Import Declarations (JEP 511)
Module import declarations simplify the importing of frequently used classes (`java.base
`) or modular libraries, without having to keep adding individual import statements to keep the compiler happy (even though IntelliJ IDEA can do this for you 😉). This feature makes things easier. You can write code without needing to worry about imports, which is useful when learning or prototyping.
Alternatively, if you have a class that imports multiple classes from a module, you can replace them with a module import statement. When you perform the Optimize imports action, the individual imports for classes imported by the module will be removed.
As your codebase grows, you might prefer to add import statements with specific imports, which you can do using the Replace with single class imports quick-fix.
If you would like to remove unused module imports when performing Optimize imports, you can configure this in the settings. Open Settings | Editor | Code Style | Java and go to the Imports tab. Then, select the Delete unused module imports option.

To see which packages are exported by a module, click on the module name in the editor or use the relevant shortcut for Go to Declaration or Usages, as shown here.
You might wonder what the difference is between module imports and wildcard imports. Wildcard imports in Java allow you to replace the import for multiple classes from the same package with one line, containing an `*
`. Module imports allow you to import classes from different packages. One downside of this is the risk of potential namespace clashes. But don’t worry, IntelliJ IDEA can help you identify and fix these, as described here.
Should you replace your current import statements with module imports? Probably not. In enterprise code, most developers prefer to have single imports. Note that IntelliJ IDEA allows you to configure the number of imports to add before replacing them with a wildcard. By default, this number is set to five. To change it, open Settings, go to Editor | Code Style | Java, and open the Imports tab. Then, set Class count to use import with ‘*’ to the desired number. Since most coding standards prefer single imports over wildcard imports, we assume the same will be true for module imports. For this reason, we are not planning to have an inspection to automatically replace existing imports with `import module java.base;
`.
However, even if you’re not using this feature explicitly, you will use it implicitly when using compact source files (described above). This feature was previously described in Java 24 and IntelliJ IDEA. For more background information, see Module Import Declarations: No More Import Hell by Mala Gupta.
Flexible Constructor Bodies (JEP 513)
With flexible constructor bodies, previously known as “statements before super()”, you are now allowed to write statements in the constructor of a derived class before calling the constructor of the super class. This is useful if you want to validate or compute data in your constructor before passing it to `super()
`, or when a superclass calls a method from its constructor that you want to override in the subclass and access a field from the subclass inside this method.
Previously, the call to `super()
` had to be the first call in the constructor. IntelliJ IDEA would give you a warning if you tried to add statements before `super()
`.

A workaround for this restriction is to call static methods inline, as arguments passed to `super()
`. While this is still possible, you now have the flexibility to call these methods before calling `super()
` with the results.
There are some limitations on which types of statements you can execute before the call to `super()
`. The statements cannot access the object under construction, which means you cannot access instance members of a class before the execution of `super()
` completes or call methods of the derived class.
This new functionality should be used responsibly. Just because you can put arbitrary code before `super()
` doesn’t mean you should move every possible validation or I/O operation into constructors. Constructors are best kept lightweight, deterministic, and free of heavy side effects. Expensive operations, retries, or external resource access are better handled in factories, builders, or initialization methods.
In short, this feature lets you model object invariants more naturally through inheritance, but it doesn’t change the golden rule – constructors should remain focused and predictable.
While this feature might not be that exciting by itself, it is a necessary step to make value classes and objects (currently in preview), as well as upcoming null-restricted value class types (currently in draft), possible. Both of these features are part of Project Valhalla. We will discuss this topic later in a separate blog post.
For a more detailed explanation of this feature and examples of how to use it, have a look at the following video featuring Dr. Venkat Subramaniam:
This feature was previously described in Java 24 and IntelliJ IDEA. For additional details and examples, see Mala Gupta’s previous blog post Constructor Makeover in Java 22. Note that this post was written when this feature was still in preview. The only significant change since then is that a constructor body is now allowed to initialize fields in the same class before invoking a constructor.
Scoped Values (JEP 506)
Scoped values are a new model for thread-local variables adapted to virtual threads. They make it possible to share immutable data within a thread and with child threads in a convenient, safe, and scalable way.
In some cases, you want to share data between components of your application or between your application and a framework, such as information about a logged-in user and their permissions. While it is possible to use thread-local variables (variables of type `ThreadLocal
`), there are several downsides to doing so, which might cause potential issues.
`ThreadLocal
` variables are mutable, which makes it hard to keep track of their current value and to reason about the code. The value of a `ThreadLocal
` variable is retained for the lifetime of a thread, unless explicitly removed by calling the `remove()
` method. Developers may forget to do so, which means data might be stored in memory longer than needed, leading to potential performance problems, as well as security issues because data might be visible to unrelated code running on the same thread. `ThreadLocal
` variables can be inherited by a child thread, but each child thread will need to create a copy of the variable, which can add to the memory footprint.
Virtual threads, added in Java 21, allow us to create many more threads than platform threads. If they each retain a copy of a thread-local variable, this will impact the memory usage. On the other hand, virtual threads may not live as long as platform threads, which minimizes the potential for memory leaks.
To use a `ScopedValue
`, you need to first declare it. It makes sense to declare it as final. Next, you need to bind the `ScopedValue
` to some data and pass a `Runnable
` or `ScopedValue.CallableOp
`, which may be realized as a lambda. This is done in the `ScopedValue.where()
` method. The operation – and any code called from it – will be able to get the `ScopedValue
`, but once it is done running, this data will be cleaned up. Scoped values have a clearly defined scope, which makes the code easier to reason about. If you try to use a `ScopedValue
` that is not bound, a `NoSuchElementException
` is thrown, as you can see in the following example.
While you cannot set a `ScopedValue
`, you can rebind it. In the example below, the variable is bound to the value `24
` inside the main method. The `update()
` method is called from here, where the value is bound to `25
` (24 + 1). When printed, it will print the current value, `25
`.
public class RebindExample { ScopedValue<Integer> JAVA_VERSION = ScopedValue.newInstance(); void main() { ScopedValue.where(JAVA_VERSION, 24).run(this::update); } private void update() { ScopedValue.where(JAVA_VERSION, JAVA_VERSION.get() + 1).run(() -> { IO.println("Hello, Java " + JAVA_VERSION.get()); // prints 25 }); } }
Scoped values are even more useful with structured concurrency, currently in its fifth preview.
Structured concurrency will make it possible for data to be automatically inherited by any threads that a thread forks. The child threads’ scope will be contained in the parent thread’s scope. With scoped values, it is no longer necessary to make copies of the data, meaning they scale very well with many (virtual) threads. This topic deserves its own blog post, where we can dive deeper into new use cases, migration paths, and performance comparisons between different approaches.
As we have seen, scoped values solve several problems associated with `ThreadLocal
`.They reduce memory overhead, improve predictability, and make reasoning about concurrent code much easier. Does this mean you should immediately rewrite all your code to use `ScopedValue
`? Not necessarily. Frameworks like Spring and others still rely heavily on `ThreadLocal
`, and migrating all existing components to Java 25 isn’t something that will happen overnight. Also, the intention behind scoped values was never to deprecate or replace `ThreadLocal
` outright, but to offer a cleaner, safer alternative. However, you might consider using scoped values for new code or modules, especially where you are already facing typical `ThreadLocal
` issues: leaks, context propagation, and cleanup.
Performance and profiler improvements
Language features are just half of the story. Java 25 also brings significant runtime improvements. Let’s take a brief look at the changes:
- Ahead-of-Time Method Profiling (JEP 515) speeds up warm-up by using method execution profile data from a prior run. The JVM can use that data immediately at startup so that hot methods are already known, reducing the delay before reaching peak performance.
- JFR Cooperative Sampling (JEP 518) and JFR Method Timing & Tracing (JEP 520) improve observability: Cooperative sampling lets threads report profiling data at safe points to reduce overhead and increase accuracy, and method timing and tracing give more precise, detailed call durations and call stack information.
- Compact Object Headers (JEP 519) shrink the object header on 64-bit JVMs from its larger experimental form to a compact 64-bit layout. This reduces memory overhead (especially with many small objects) and improves garbage collection (GC) and cache behavior.
- Generational Shenandoah (JEP 521) adds generational GC support to the Shenandoah garbage collector so that young-generation objects can be collected more efficiently, reducing pause times and improving throughput for workloads with many short-lived objects.
Moving from Java 21 to Java 25
If you are upgrading to Java 25 from Java 21, here is an overview of some of the stable features you might not be familiar with yet.
Stream Gatherers (JEP 485)
Stream Gatherers were added in Java 24. They improve the Stream API, added in Java 8. Stream gatherers allow you to add your own custom intermediate operations to a stream.
For an explanation of this feature and how you can use it, watch the following livestream with José Paumard:
If you’d like more background information on this feature, you might be interested in the following video we created when this feature was still in preview:
Finally, we highly recommend this video from JavaOne by Viktor Klang, the creator of the feature:
Markdown Documentation Comments (JEP 467)
Markdown documentation comments were added in Java 23. As the name suggests, it is now possible to use Markdown in your JavaDoc. Back when Java was first created, HTML seemed like a logical choice for JavaDoc, but these days you might prefer Markdown.
IntelliJ IDEA supports the adoption of this feature by offering a quick-fix to convert JavaDoc to Markdown. If you have JavaDoc that you would like to convert, press Alt+Enter when the cursor is on your JavaDoc and select the Convert to Markdown documentation comment option.
Does this mean you should convert your existing JavaDoc to Markdown? Not necessarily. You might just consider writing your documentation in Markdown from now on. However, should you want to convert it, IntelliJ IDEA can help you.
For more information on this feature, have a look at Markdown in Java Docs? Shut Up and Take My Comments! by Mala Gupta.
If you’re interested in more background on this feature, check out the video that was created when it was still in preview:
Unnamed Variables & Patterns (JEP 456)
Unnamed variables and patterns make it possible to use unnamed variables and unnamed patterns when variable declarations or nested patterns are required but the actual variables or patterns are not used. Denoting unnamed variables and patterns with an `_
` clearly conveys that they are not used elsewhere in the code. IntelliJ IDEA will detect when an unused local variable could be replaced with an underscore `_
` and offer a quick-fix to do so.
For more details on this feature, see Drop the Baggage: Use ‘_’ for Unnamed Local Variables and Patterns in Java 22 by Mala Gupta.
Virtual Threads (JEP 444) and Synchronize Virtual Threads without Pinning (JEP 491)
Added in Java 21, virtual threads are lightweight threads. Unlike platform threads, which are limited by the number of cores in your machine, you can create a potentially unlimited number of virtual threads. Virtual threads improve the scalability of your applications. They do need platform threads to perform their work, but can release them when they are waiting for blocking code. Tooling like jcmd and IntelliJ IDEA can collect thread dumps with virtual threads included.
Synchronizing virtual threads without pinning (added in Java 24) improves the scalability of Java code that uses `synchronized
` code. Virtual threads that block in `synchronized
` blocks now release their underlying platform threads so they can be used by other virtual threads. This last improvement comes for free when you upgrade from Java 21 to Java 24 or higher. To see this in action, watch the demo we did in the What’s New in IntelliJ IDEA 2025.2 livestream.
Runtime and security improvements
Java 24 introduced some changes related to post-quantum cryptography readiness. The Java platform is preparing for the future of secure computing:
- Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism (JEP 496) and Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm (JEP 497), introduced in Java 24, are NIST-standardized post-quantum algorithms (lattice-based) aimed at replacing or augmenting classic public-key operations like RSA and ECDSA. Java also receives a performance boost. Garbage collection, memory footprint, and startup times all see measurable improvements:
- Region Pinning for G1 (JEP 423) allows the G1 garbage collector to continue collecting parts of the heap even when some regions are in JNI critical sections. Instead of stalling GC during such critical regions, only “pinned” regions are excluded, reducing latency in mixed JNI/native workloads.
- The Foreign Function & Memory API (JEP 454), added in Java 22, offers better performance and safety over the old JNI for foreign memory and calling native code. Bulk operations are more efficient, with less overhead in cross-boundary calls.
- ZGC: Generational Mode by Default (JEP 474). The Z Garbage Collector, which is typically low-pause, uses generational mode by default starting with Java 23. Separation between young and old objects improves GC efficiency for applications that create many short-lived objects.
Additionally, there are further enhancements to core libraries, previews, and performance ergonomics – for example, improvements to default GC settings, memory management, etc.
Upgrading from Java 21 to 25
You can find a video series that covers the Road to Java 25 on the official Java YouTube channel. We recommend watching How to Upgrade to Java 25 by Nikolai Parlog. The Oracle DevRel team also has a Java 25 livestream on September 16, 2025.
Conclusion
As you have seen, several additions to the language make Java easier to use, both for students and teachers, as well as experienced programmers. In fact, these features completely change the experience of using Java, by no longer requiring boilerplate code like the `public static void main(String[] args)
` method, allowing statements before `super()
`, offering the possibility to add custom intermediary operations in streams using gatherers, allowing underscores as variable names for unused variables, and and providing scoped values as a convenient, safe, and scalable alternative to `ThreadLocal
`. With all of these changes, Java is moving forward fast. IntelliJ IDEA aims to move just as quickly by supporting these new features with relevant inspections and quick-fixes, as well as new live templates, and integrating them into existing features like the debugger. Other than improvements in the language, there are also runtime improvements that you get for free when upgrading your JDK.
We think Java 25 is the best Java release (so far!) and we recommend you switch to it as soon as you can. Even if you’re not using the Java language features, you will still benefit from using the new JDK. Java 25 offers a new baseline for the language, and the runtime is the fastest it has ever been.
If you are on Java 21, we have provided an overview of what you might have missed in terms of language features. And if you’re on an even older version of Java, now would be the time to start the process to upgrade, not just because of the benefits to you but also because the ecosystem is moving forward. Java 17 is the current baseline for Spring and Spring Boot as of Spring 6 or Spring Boot 3. The upcoming Maven 4 will require JDK 17 to run. But don’t worry, you can still compile projects with older Java versions! The upcoming JUnit 6 is also targeting JDK 17. If you are still on Java 8 (or older), now is the time to start upgrading!
IntelliJ IDEA will continue to support the latest Java features with Java 26. If you would like to try them out, you can download the EA versions from inside IntelliJ IDEA as soon as they become available. And, as always, please let us know if you have any feedback.