What’s New in Java 10?
Want to learn more about the new features of Java 10 and OpenJDK? Check out this post on the new local variable type inference, unmodifiable collections, and more!
Join the DZone community and get the full member experience.Join For Free
Java 10 Overview
In March 2018, we saw the latest semi-annual release of Java — Java 10.
In this article, we’ll examine the big changes introduced in this version, as well as talk about some of the smaller improvements that will make life easier for developers and ops alike.
Java 10: Big Changes
The two big stories in Java 10 are:
- the new
varkeyword, just like you’d imagine with any new language construct, and
- the new six-month release cycle
Also, developers will be excited to see more API evolution. And, there are runtime improvements, new performance tuning knobs, and the now-perennial garbage collection improvements that we’ve come to expect with each release.
But there are a number of other interesting things, too, especially if you know how to read in between the lines and look ahead to Java 11 in September.
Local Variable Type Inference
With the exception of
assert from the Java 1.4 days, new keywords always seem to make a big splash, and
var is no different.
Perhaps, the most curious thing about it is that it isn’t actually a reserved word, but something else entirely. But, there will be more on that in a moment.
var keyword does is turn local variable assignments...
HashMap<String, String> ughThisIsSoVerbose = new HashMap<>();
var succinct = new HashMap<String, String>();
Simply put, as long as the construct on the right-hand side doesn’t require a target type on the left-hand side (like lambdas do), then, you can make all kinds of code easier to read, as shown below:
var tshirts = Lists.of("Baeldung Medium", "Java Large", "Lua Small"); var lines = Files.get(Paths.get("log/catalina.out")); var length = lines.count();
In other words, Java 10 introduces local-variable type inference to the language. At compile time, it figures out the reference type based on the value type.
Now, we can append this to the growing list of type inferences that Java makes, already including type inference with generics and with lambda expressions.
This feature has been a long time coming. It was suggested as far back as 2001 and was closed at that time with the following comment by Gilad Bracha.
Humans benefit from the redundancy of the type declaration in two ways. First, the redundant type serves as valuable documentation — readers do not have to search for the declaration of
getMap()to find out what type it returns. Second, the redundancy allows the programmer to declare the intended type and, thereby, benefit from a cross-check performed by the compiler.
Times have changed, though, and the Java language is learning about the benefits of choice. For example, there are situations where
vars added succinctness and may make the code harder to read:
var x = someFunction();
The above snippet is completely valid Java 10 code, and it’s absolutely confusing to read.
It’s confusing, because it’s impossible for the reader to tell x‘s type without tracking down
someFunction‘s return type. Similar complaints have been levied against dynamically-typed languages for years.
And, of course, this specific usage is precisely what Gilad warned the community about over 15 years ago.
var with care, and remember that the goal is to write readable code.
It's Actually Not a Reserved Word
Don’t let folks tell you it is a reserved word. Under the hood,
var is a special new type in Java.
So, actually, you can still use var in other places of your code, say as a variable or class name. This allows Java to remain backward compatible with pre-Java 10 code that may have made the interesting choice of naming a variable or two "var."
And, there is a lot more to the story! Read up on using
var with non-denotable types, as well as var's limitations surrounding polymorphism and lambda expressions in Oracle’s guide to local-variable type inference.
Unmodifiable Collection Enhancements
To introduce this next enhancement, consider the following Java puzzler. What is the value of v at the end of this program?
var vegetables = new ArrayList<>(Lists.of("Brocolli", "Celery", "Carrot")); var unmodifiable = Collections.unmodifiableList(vegetables); vegetables.set(0, "Radish"); var v = unmodifiable.get(0);
The answer, of course, is Radish. But, isn’t unmodifiable, well, unmodifiable?
Unmodifiable vs Unmodifiable View
Actually, according to Java 10’s updated
Collection Javadoc, the
unmodifiableList returns an unmodifiable view collection:
An unmodifiable view collection is a collection that is unmodifiable and is also a view onto a backing collection.
Examples of unmodifiable view collections are those returned by the
Collections.unmodifiableList, and related methods.
Note that changes to the backing collection might still be possible, and if they occur, they are visible through the unmodifiable view.
But, let’s say that you want something genuinely unmodifiable, what would you do?
Will the Real Unmodifiable Methods Please Stand Up?
Well, Java 10 adds two new APIs to make this possible, that is, to create collections that can’t be modified at all.
The first API allows unmodifiable copies to be made of collections by adding the
var unmodifiable = List.copyOf(vegetables);
That is different from wrapping a list in
Collections.unmodifiableList . The
copyOf performs a shallow copy in iteration order. Changes to the
vegetables won’t be manifested in
unmodifiable now. Whereas, they are with our original approach.
The second API adds three new methods to the
Collectors class in the Streampackage. You can now stream directly into an unmodifiable collection using
toUnmodifiableSet , and
var result = Arrays.asList(1, 2, 3, 4) .stream() .collect(Collectors.toUnmodifiableList());
Note that while these method names may remind you of
Collections.unmodifiableList and the like, these new methods produce genuinely unmodifiable lists, while
Collections.unmodifiableList returns an unmodifiable view.
Java 9 made the Garbage-First Garbage Collector (G1GC) the default, replacing the Concurrent Mark-Sweep Garbage Collector (CMS). Java 10 introduces performance improvements to G1GC.
In Java 10, G1GC is getting a performance boost with the introduction of
full parallel processing during a Full GC. This change won’t help the best-case performance times of the garbage collector, but it does significantly reduce the worst-case latencies. This makes pauses for garbage collection far less stressful on application performance.
When concurrent garbage collection falls behind, it triggers a Full GC collection. The performance improvement modifies the full collection so that it is no longer single-threaded, which significantly reduces the time needed to do a full garbage collection.
Application Class-Data Sharing
Java 5 introduced Class-Data Sharing (CDS) to improve startup times of smaller Java applications.
The general idea was that, when the JVM first launched, anything loaded by the bootstrap classloader was serialized and stored in a file on disk that could be reloaded on future launches of the JVM. This meant that multiple instances of the JVM shared the class metadata, so it wouldn’t have to load them all every time.
The shared-data cache meant a big improvement in startup times for smaller applications, because, in that case, the relative size of the core classes was larger than the application itself.
Java 10 extends this to include the system classloader and the platform classloader. To take advantage of that, you just need to add the following parameter:
Adding Your Own Classes to the Archive
But, the bigger change is that it allows you to store your own application-specific classes into the Class-Data Sharing cache, too, possibly decreasing your startup times.
Basically, it is a three-step process. The first step is to create the list of classes that should be archived by starting up your application with the appropriate flags and indicating where you want the list to be stored:
java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=myapp.lst \ -cp $CLASSPATH $MAIN_CLASS
Then, with this list, you will create a CDS archive:
java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=myapp.lst \ -XX:SharedArchiveFile=myapp.jsa \ -cp $CLASSPATH
And, finally, run your app, using that archive:
java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \ -cp $CLASSPATH $MAIN_CLASS
New Just-in-Time Compiler
The Just-In-Time (JIT) Compiler is the part of Java that converts Java bytecode into machine code at runtime. The original JIT Compiler was written in C++ and is now considered quite difficult to modify.
Java 9 introduced a new experimental interface called the JVM Compiler Interface or JVMCI. The design of the new interface makes it possible to rewrite the JIT Compiler in pure Java. Graal is the resulting JIT Compiler, written entirely in Java.
Graal is currently an experimental JIT compiler. Only Linux/x64 machines can use it until future releases of Java. To enable Graal, add these flags to your command line arguments when starting the application:
And, keep in mind that the Graal team makes no promises in this first release that this compiler is any faster. The driving hope is that Graal will help evolve the JVMCI and make future maintenance tractable.
Among the performance improvements in the JVM, there is a subtle-but-powerful one referred to as Thread-Local Handshakes.
During serviceability operations, like collecting stack traces for all threads or performing garbage collections, when the JVM needed to pause one thread, it needed to stop them all. Sometimes, these are referred to as “stop-the-world” pauses. This was due to the JVM wanting to create a global safe-point from which all application threads could begin again once the JVM was done.
In Java 10, though, the JVM can put an arbitrary number of threads into a safepoint, and threads may continue running after performing the prescribed “handshake." This resulted in the JVM being able to pause just one thread at a time, whereas before it had to pause them all.
To be clear, this isn’t a feature directly available to developers, but it is one everyone will enjoy.
A Forerunner for Big GC Changes
And, if you’re following closely, you’ll also see that this is related to an upcoming (and experimental) low-latency garbage collector coming in Java 11, which clocks GCs at only 10ms. It’s also a cousin to the very-cool no-GC option coming in Java 11 as well.
The JVM now knows when it is running inside a Docker Container. This means the application now has accurate information about what the docker container allocates to memory, CPU, and other system resources.
Previously, the JVM queried the host operating system to get this information. This causes a problem when the docker container would actually like to advertise a different resource set.
For example, let’s say that you wanted to create a Java-based docker image where the running JVM was allocated 25 percent of the available memory specified by the container. On a box that has 2G of memory, running a container configured for 0.5G of memory, Java 9 and earlier would incorrectly calculate the Java process’s heap size based on the 2G number instead of 0.5G.
But, now, in Java 10, the JVM is capable of looking up this information from container control groups (cgroups), which is where Docker places these details.
There are command line options to specify how the JVM inside a Docker container allocates internal memory. For instance, to set the memory heap to the container group size and limit the number of processors, you could pass in these arguments:
With containers becoming a standard way to deploy services, this means developers now have a container-based way to control how their Java application uses resources.
Alternative Memory Allocation
Java is moving toward a more heterogeneous memory system by allowing users to specify alternative memory devices to allocate the heap.
An immediate use case is being able to allocate heap on a Non-Volatile DIMM (NVDIMM) module, which is commonly used in big Data applications.
Another use case is where many JVM processes are running on the same machine. In this case, it might be good to have processes that require a lower read latency map to DRAM and the remaining processes mapped to NVDIMM.
To use this, add this flag to your startup parameters:
For this parameter,
path would typically be a memory-mapped directory.
Easier SSL With OpenJDK
The open-source version of Java 10, OpenJDK, also received some great news regarding root certificates. Java ships with a key-store called
cacerts, which is a home for root certificates for Certificate Authorities that the JVM can use to perform SSL handshakes and the like. But, in OpenJDK, this key-store has always been empty, relying on the user to populate it.
This extra maintenance makes OpenJDK a less attractive choice if your application needs to open SSL sockets. However, Oracle decided in this release to open-source the root certificates issued by Oracle’s Java SE Root CA Program so that they could now be included in the open-source version of the JDK. Basically, this means that doing simple things, like communicating over HTTPS between your application and, say, a Google RESTful service, will be much simpler with OpenJDK.
Feel free to check out the difference by using
keytool to list the certs in
keytool -cacerts -list
If you are using OpenJDK 9 or earlier, this will be empty, but with OpenJDK 10, it will be flush with certificates from Digicert, Comodo, Docusign, and many others.
The New Release Cycle
Aside from just a project management mechanism, Java 10 actually changes the version numbering schema inside class files.
You’ve all seen an exception like this before:
Unsupported major.minor version 52.0 at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:791) ...
Of course, when you received this exception, if you were keeping track, you knew that this meant that you were trying to run a Java 8 library on a Java 7 JVM or earlier, because 52.0 meant Java 8, just like 51.0 meant Java 7.
Now, though, the new numbering system has semantic meaning. It will look something like this:
FEATURE refers to the version of Java. So, in Java 10’s case,
FEATURE is 10. It will increment every six-months, matching the new Java release cycle.
INTERIM is actually reserved for future “interim” cycles. For example, if Java wanted to start releasing faster than every six months. For the time being, it will always be 0.
UPDATE is a bit odd. It begins at 0 and one month after the last FEATURE release, it bumps up to 1. And, then, it increments every three months after that. So, that means that with Java 10, in April 2018,
UPDATE was 1. In July 2018, it is 2, and in September, it is 3, incrementing until Java 10 is EOL.
PATCH is any release that needs to happen in between
UPDATE increments, for example, critical bug fixes.
Additionally, version numbers remove trailing zeros. So, that means that the version string when Java 10 went live was simply 10.
In April, Oracle released 10.0.1, and in July, it released 10.0.2. You can check out the release notes for both on their website.
The Java 10 release includes additional bug fixes and performance improvements. The biggest performance boost was in the startup time of the JShell REPL tool. This will make working with the tool more responsive.
Java 10 is the first new release made of the JDK on the new 6-month release cycle.
Each release from now on will have fewer large features, but they will come much faster. This means that if a major feature misses a release, it will most likely be released only 6 months later. The original release cycle could have pushed a new feature out several years.
This time, some of the major features of the release were parallelized garbage collection, local variable type-inference, and the new release cycle numbering schema. Finally, for more details be sure to check out the official Java 10 release notes.
Published at DZone with permission of Eugen Paraschiv, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.