In an interesting alignment of events, the twenty-fifth version of Java, JDK 25, is being released in 2025, which also happens to be the 30th anniversary of the platform’s launch.
This seems like an opportune time to have a look back at how the Java language has evolved and what new features are included in this latest release.
The launch of Java officially happened on May 23rd, 1995. As we all know, Java’s popularity took off and, even today, continues to be one of the three most popular programming languages.
Before we get into the evolution of Java, it’s worth spending a moment looking at the primary influences. There are five that I’ll highlight:
- Lisp. Even though this dates back over thirty years before Java, it was the first language to use garbage collection for automated memory management, a key feature of the JVM. Lisp is also viewed as the first language to use just-in-time (JIT) compilation, another prominent feature of the JVM, although not added until JDK 1.2.
- Simula 67. This was the first object-oriented programming language and, incidentally, the first programming language James Gosling (the Father of Java) learnt.
- The C programming language. When Java was being developed, this was one of the most popular programming languages. To make it easier for developers to adopt Java, much of the basic syntax is lifted directly from C. As we’ll see, this reuse of features from other languages is an enduring trait of the way Java has evolved.
- Pascal. The first language to use a true interpreter to execute pCode through a virtual machine. The JVM, and its ability to deliver internet-scale performance and scalability, is one of the primary reasons Java remains so popular.
- The World Wide Web. Early browsers were only able to display static information, often needing to launch external applications to view certain content types. Solving this problem and delivering programmable, dynamic content in web pages was why people initially became so excited about the possibilities Java offered.
The reality is that when Java was launched, it didn’t contain anything revolutionary. All the main features, as described above, had already been used in other, earlier languages. However, what it did do was bring all these things together in a single platform. It was also the right language at the right time, specifically to help drive the adoption of the Internet and the World Wide Web.
James Gosling described Java as a “blue-collar programming language”. He didn’t want it to be an academic exercise in what new directions a language could take; he wanted it to be a language that could get the job done. Quickly and easily. Thirty years later, it’s fair to say that this philosophy is still rigorously applied when deciding which new features to add.
The first Java Development Kit (JDK) was very basic compared to JDK 25. The virtual machine contained only an interpreter, which meant that, compared to natively compiled languages like C and C++, Java was slow. Very slow. The way GC was performed was also quite rudimentary, using just a basic mark and sweep algorithm. Since this was a stop-the-world collector, not only was Java slow to execute, but it would periodically stop doing anything while the JVM reclaimed unused memory. In some ways, it was amazing that Java became so popular!
The other thing that JDK 1.0 lacked was a comprehensive set of core class libraries. The first release only contained 211 library classes. There was no swing, no collections library and no concurrency utilities.
Thankfully, Java developed quickly, so let’s look at the key changes in various releases.
JDK 1.1: In the first update, the primary new features were inner classes, JDBC for database access and reflection. The JVM got JIT compilation, but only on the Windows platform.
JDK 1.2: This included the Swing graphical framework (a vast improvement on AWT), the Collections framework, as well as the first new keyword in the form of strictfp (which, incidentally, became obsolete in JDK 17). JIT compilation was extended to all platforms.
JDK 1.3: The primary change in this release was the inclusion of the HotSpot JVM by default. Released in 2000, this is the origin of the C2 JIT compiler, which is still used in OpenJDK today.
JDK 1.4: Another new keyword, assert. Several new libraries were also added, specifically new IO (NIO), logging, regular expressions, preferences and basic XML parsing.
JDK 5: This was a big set of changes for Java (not least because we jumped from JDK 1.4 to 5). The language itself got seven new features:
- Generic types: The implementation of these has always been somewhat contentious. To maintain a high level of backward compatibility, type erasure is used, so, for example, at runtime, it is not possible to differentiate between a List<String> and a List <Number>; they are both a raw List type.
- Annotations: Something that has been an enormous benefit, especially when considering enterprise frameworks like Jakarta EE, Spring, Hibernate, etc.
- Autoboxing and unboxing. Automatic conversion from primitives to wrapper classes and vice versa. Although useful, this can sometimes lead to code that is harder to read as the boxing and unboxing happen transparently.
- Enumerations: Another new keyword, enum, to support type-safe ordered lists of values.
- Varargs: Declaration of the final parameter to a method to allow any number of arguments to be passed.
- Enhanced for loop: This simplifies iterating over an array or class that implements the Iterable interface.
- Static imports: Eliminating the need to provide a fully qualified class name when using static members of the class.
The significant library change in JDK 5 was the inclusion of the Concurrency Utilities, a set of higher-level abstractions, such as Semaphore and Mutex, for writing co-operative multi-threaded code. Also related to that was the introduction of the new memory model for Java. Although this didn’t have any direct programming implications, it did address issues inside the JVM to improve reliability and performance.
JDK 6: This was probably the least interesting Java release, with no changes to the language, mostly just library version upgrades and JVM changes to deliver better performance.
JDK 7: Another significant set of changes to the language through Project Coin, which delivered:
- Strings in switch: supplementing integral values and enumerations.
- The try-with-resources statement: A massive benefit for writing simpler and more reliable code that handles interactions with connections that need to be explicitly closed.
- The diamond <> operator: Using type inference to eliminate the need to specify the generic type twice, once for the variable declaration and once for the object instantiation.
- Binary integer literals: It’s hard to believe it took fourteen years to include the ability to write a binary number in Java!
- Underscores in numeric literals: Breaking up long numbers to make them more readable.
- Multiple catch blocks for exceptions: eliminating code repetition for different exceptions.
More concurrency libraries were also added, along with extensions to the NIO packages to support multiple file systems.
Another significant library feature was the fork-join framework. This continued to make writing co-operative multi-threaded code easier, in this case, for situations where a large job could be recursively split into smaller jobs.
JDK 7 also included the first new bytecode in the JVM since its launch. The invokedynamic instruction was not used in Java but was intended to make it easier to compile dynamically typed languages like Ruby to run on the JVM.
JDK 8: This was undoubtedly the biggest release since JDK 5 and was probably more significant.
The reason for this was the inclusion of lambda expressions and the Streams API. Used together, these introduced an element of functional programming to Java, something that had not been present previously.
Streams enabled the common pattern of filter-map-reduce to be applied to sets of data. Lambda expressions greatly simplify the definition of a functional interface, which eliminates much of the boilerplate code required for an anonymous inner class. The lazy evaluation of streams, combined with the simple ability to switch a stream between serial and parallel (which really should have been named concurrent), made the performance of streams very attractive.
Streams and lambdas generated a lot of enthusiasm for Java as it approached its twentieth anniversary.
JDK 9: This was also a significant release, but for different reasons.
Firstly, Oracle declared that this would be the “last major release of Java”. This was a result of the decision to move away from a feature-based release model to a time-based one. Rather than waiting for all features to be ready before making a release (which had led to anything from two to over four years between releases), a release would happen every six months. Features that were ready would be included; any that weren’t would just be delayed by six months (or until they were ready).
The second reason JDK 9 was significant was Project Jigsaw. This had originally been planned for JDK 7, then delayed to JDK 8 and even led to more than one delay in the release of JDK 9. Jigsaw introduced a module system for applications and, more importantly, modularised the core class libraries. As I mentioned, JDK 1.0 had 211 classes in the libraries; by JDK 8, that had risen to over four and a half thousand. Putting them all in the rt.jar file impacted both performance and security. JDK 9 also included a new command, jlink, which could be used to generate a JDK image that only contained the modules required to run an application. The Java Runtime Edition (JRE) was no longer part of the JDK, as it wasn’t necessary.
Tied into this was the idea of encapsulating all the internal JDK APIs. These had never been publicly documented, and developers were warned not to use them as they could change or be removed without notice. This did not, however, stop people from using them. The most famous (or infamous) of these was the sun.misc.Unsafe class (the clue really is in the name). Methods in this class allowed developers to sidestep some of the protection mechanisms of the Java platform, but, by doing so, could deliver significant performance advantages. Frameworks like Spring had done this, so suddenly removing access would stop all Spring-based applications running in a single move.
Since the Java developers had always been meticulous about backwards compatibility, this was a serious issue. As a result, the Java Community Process (JCP) Executive Committee took the unprecedented step of voting down the JSR for Java SE 9 at its first attempt. Changes were made to restore internal API access via command line flags, and JDK 9 was released to the general public.
Since then, all the major frameworks and libraries have worked towards eliminating internal JDK dependencies, but JDK 9 remains the biggest hurdle for migrating applications to a newer JDK (or at least those that do not use just the standard Java libraries). As part of the library changes, JDK 9 included var handles, which provided a supported API for some aspects of the Unsafe class.
JDK 9 also introduced jshell as a way of fast prototyping small pieces of code.
JDK 10: With the new six-month release cadence, the rate of change of Java actually increased, but individual releases did not contain so many new features.
JDK 10 included local variable type inference. Rather than explicitly specifying the type of a variable where it was immediately assigned to an object reference, the var keyword could be used, and the compiler would infer the type automatically. Unlike strictfp, assert and enum, var was not added as a new reserved word (think of all the code that would have broken!) Instead, it was added as a reserved type, so you could no longer define a class called var (not that you ever would have).
JDK 11: This included several minor changes, but no major new features.
One notable aspect of JDK 11 was that the Oracle JDK aligned with OpenJDK in terms of features. Prior to this, the Sun and then Oracle JDK had included what were classified as commercial features that were not part of OpenJDK. Oracle JDK 11 dropped support for the Applet Browser plugin, Java Web Start and the JavaFX GUI library, as well as a few minor items.
JDK 12: This included the first Preview Feature in the JDK. Quoting directly from JEP 12, which defines preview features:
“A preview feature is a new feature of the Java language, Java Virtual Machine, or Java SE API that is fully specified, fully implemented, and yet impermanent. It is available in a JDK feature release to provoke developer feedback based on real-world use; this may lead to it becoming permanent in a future Java SE Platform.”
The ability to include preview features (and incubator modules for library APIs) is a direct result of the switch to a six-month release cadence.
In JDK 12, Switch Expressions provide an alternative way of using a switch, which returns a value determined by the matching case (or throws an exception). This eliminates a lot of boilerplate code in the switch statement and minimises potential errors, like forgetting to include a break statement for a case block.
JDK 13: This is the smallest release of Java by number of JEPs, but does include text blocks. This allows a string to be delimited by triple quotes that can include line breaks and special characters. This is another example of where the Java developers have looked at other languages and adopted a feature. The syntax of text blocks is the same as that used in Python.
JDK 14: This included two new preview features: records and pattern matching for instanceof.
Records are a new kind of type declaration, in the same way that enumerations were. While still a fully functioning Java class, records eliminate much of the boilerplate code associated with defining a simple data-carrying class (one that just encapsulates state without any specific behaviour).
Pattern matching is a language feature that has been around almost since the first programming languages and, as we’ll see, has gradually been included in different ways since JDK 14. When used with instanceof, the (rather redundant) requirement to assign the tested reference to the specific type with an explicit cast is removed.
A new library, the Foreign Memory Access (FMA) API, was included, which is part of Project Panama. Panama is a replacement for the Java Native Interface and is intended to make using non-Java libraries from Java code much simpler. This was included as an incubator module.
My favourite feature in JDK 14 was helpful NullPointerExceptions. When chaining method calls, the origin of the NullPointerException is not clear. This change added detail to the exception message to make this obvious. Again, the time it took to make this relatively simple change is quite amazing.
JDK 15: Sealed classes were introduced, giving developers more control over the inheritance of a given class. When combined with records, sealed classes provide a form of algebraic data types in Java.
JDK 16: No new language features, but some new libraries, again initially as incubator modules.
The Foreign Linker API was complementary to the FMA API and also part of Project Panama.
The Vector API (not to be confused with the Vector class) provides a way to explicitly express how numerically intensive operations can use the single-instruction-multiple-data capabilities of the underlying processor. The JIT can autovectorise some code patterns, but not all. The Vector API can deliver significant performance improvements by overcoming this limitation.
JDK 16 also changed the default for how the JDK internal APIs are exposed. The default value for the –illegal-access flag changed from permit to deny (but could still be overridden). Access to sun.misc.Unsafe is still possible.
JDK 17: Pattern matching for switch extended the switch statement and expression. In addition to integral values, strings and enumerations, it is now possible to match on a type and have a variable assigned for its use.
The Foreign Memory Access API and Foreign Linker API were combined into a single library (still an incubator module) called the Foreign Function and Memory API.
Access to the JDK internal APIs was also taken to the next level. The –illegal-access flag no longer has any effect, so the setting is fixed at deny. Access to sun.misc.Unsafe is still possible.
JDK 18: There were no new language features or libraries introduced in JDK 18, although finalization, a perennial thorn in the side of Java, was deprecated for removal. Several preview features and incubator modules went through further revision.
JDK 19: Pattern matching was extended to records. This uses a deconstruction pattern so values inside the record can be accessed without explicit method calls.
The prominent feature in JDK 19 was virtual threads. Prior to this, each Java thread was mapped to an underlying operating system thread (or platform thread). For common internet server applications that use a thread-per-request programming model, this could be a severe limitation on scalability. Virtual threads effectively introduced continuations to Java and allowed multiple Java threads to map to a single platform thread. This was combined with a new incubator module, the Structured Concurrency API. Both were part of Project Loom.
JDK 20: No new language features and only one new incubator module, also part of Project Loom, which was Scoped Values. Other APIs and virtual threads continued their development under incubation.
JDK 21: This contained two new language features:
- String Templates. This supplements the existing methods for constructing string literals when combining static text with evaluated expressions.
- Unnamed patterns and variables. This simplifies code by allowing the use of an underscore to indicate that a variable is present (such as in a record pattern), but is not going to be used.
There was also a small addition to the Collections API in the form of Sequenced Collections, which are ones that have a defined encounter order.
JDK 22: One change to the language in the form of statements before super(). This gives greater freedom to developers when defining a constructor for a class.
Additionally, the Stream API now includes Gatherers. This allows the definition of custom intermediate operations in the same way that custom collectors can be defined for the terminal operation.
JDK 23: An interesting release because the String Template feature introduced in JDK 21 was removed, which demonstrates the power of preview features. After consideration of feedback, it was determined that templates in their current form were not appropriate and required a rethink.
There was one addition to the language: primitive types in patterns, instanceof and switch. There has always been tension in the Java language, as it is not truly object-oriented. To improve performance, it includes primitive types for numerical values, characters and booleans, which require wrapper classes to treat them as objects. The introduction of autoboxing and unboxing reduced some boilerplate code at the expense of some less-than-obvious edge cases.
In JDK 23, the use of primitive type patterns is extended to include both the instanceof operator and the switch statement and expression. Primitive types could already be used in record patterns since their introduction in JDK 19.
JDK 24: Although this contained 24 JEPs, none of them introduced new language features. There was one significant change in the libraries to enable virtual threads to be used more effectively. This lifted the restriction preventing a Java thread from being unmounted from a platform thread if it was in a synchronized block or method.
Which brings us up to date on how Java has changed over thirty years.
Java has clearly not remained static. Through the OpenJDK project, JEPs and the relevant JSRs under the Java Community Process, Java has adapted and evolved to address the changing needs and preferences of many millions of developers. Enormous credit should be given to the architects of the OpenJDK, who have integrated often complex and foreign features in a way that has not broken the feel of Java.
What lies in store, then, for the immediate future of Java? Let’s dig into what JDK 25 has to offer. There are eighteen JEPs, which is a little higher than the average of thirteen.
There are no new language features in JDK 25, although what was introduced as statements before super() in JDK 22 as a preview feature now becomes final as Flexible Constructor Bodies. Primitive types in Patterns, instanceof, and switch continue to develop under a third preview release.
There are two additions to the core APIs.
Stable Values. This is an interesting API that addresses limitations of creating immutable fields in Java using the final keyword.
PEM Encodings for Cryptographic Objects. This provides methods to encode and decode cryptographic keys, certificates, and certificate revocation lists into the Privacy-Enhanced Mail transport format.
Several other APIs go through either further refinement or are made final:
- Structured concurrency (fifth preview)
- Scoped Values (now final)
- Vector API (tenth preview)
- Key derivation function API (now final)
As a side note, it’s worth pointing out that the reason the Vector API is still in preview after ten iterations is because it is part of the larger Project Valhalla. This will add value types to Java, and the Vector API will only become final when core pieces of Valhalla are delivered.
As we’ve seen, Java has evolved in many ways over its thirty-year history. Through the OpenJDK and the JCP, using both JEPs and longer-term projects, this pace of change will continue for the foreseeable future. All of which will help Java to maintain its place as one of the most popular programming languages in the world.