Well, another six months have passed, and we have another release of Java, this one pretty packed with exciting new features. It is, therefore, time for another blog post trying to list everything new in JDK 14.
In total, there are a very impressive 16 JDK Enhancement Proposals (JEPs) and 69 new API elements.
Let’s start with the more significant items that introduce changes to the Java language syntax.
Java is an object-oriented language; you create classes to hold data and use encapsulation to control how that data is accessed and modified. The use of objects makes manipulating complex data types simple and straightforward. It’s one of the reasons Java is so popular as a platform.
The downside (until now) is that creating a data type has been verbose, requiring a lot of code even for the most straightforward cases. Let’s look at the code needed for a basic two-dimensional point:
public class Point {
private final double x;
private final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
}
That’s 14 lines of code just to represent a two-value tuple.
JDK 14 introduces records as a preview feature. Preview features are a new concept that allows the developers of the Java platform to include a new language feature without making it part of the Java SE standard. By doing this, developers can try out these features and provide feedback allowing changes (or even the removal of a feature), if required, before the feature becomes set in the standard. To use a preview feature, you must specify the command line flag, --enable-preview
for both compilation and runtime. For compilation, you must also specify the -source
flag.
A record is a much simpler way of representing a data class. If we take our Point example the code can be reduced to a single line:
public record Point(double x, double y) { }
This takes nothing away from the readability of the code; we’re immediately aware that we now have a class that contains two double values called x and y that we can access using the standard accessor method names of getX and getY.
Let’s examine some of the details of records.
To start with, records are a new kind of type that is a restricted form of class in the same way that an enum is. A record has a name and a state description, which defines the components of the record. In the Point example above, the state description is the doubles, x and y. Records are designed for simplicity so they cannot extend any other class or define additional instance variables. All state in a record is final, so no accessor (setter) methods are provided. If you need any of that, you need to use a full-blown class.
Records do have flexibility, though.
Often, the constructor needs to provide additional behaviour beyond just assigning values. If this is the case, we can provide an alternative implementation of the constructor:
record Range(int min, int max) {
Range {
if (min > max)
throw new IllegalArgumentException(“Max must be >= min”);
}
}
Note that the constructor definition is still abbreviated, as specifying the parameters is redundant. Any of the members that are automatically derived from the state description can also be declared so, for example, you can provide an alternative toString()
or hashCode()
implementation.
In some situations, you do not know the exact type of an object. To handle this, Java has the instanceof operator that can be used to test against different types. The drawback to this is that, having determined the type of an object; you must use an explicit cast if you want to use it as that type:
if (o instanceof String) {
String s = (String)o;
System.out.println(s.length);
}
In JDK 14, the instanceof operator has been extended to allow a variable name to be specified in addition to the type. That variable can then be used without the explicit cast:
if (o instanceof String s)
System.out.println(s.length);
The scope of the variable is limited to where its use is logically correct so:
if (o instanceof String s)
System.out.println(s.length);
else
// s is out of scope here
The scope can also apply within the conditional statement so we can do something like this:
if (o instanceof String s && s.length() > 4) ...
This makes sense since the length() method will only be called if o is a String. The same does not work with a logical or operation:
if (o instanceof String s || s.length() > 4) ...
In this case, s.length() needs to be evaluated regardless of the result of whether o is a String. Logically, this does not work and so will result in a compilation error.
Using the logical not operator can produce some interesting scoping effects:
if (!(o instanceof String s && s.length() > 3)
return;
System.out.println(s.length()); // s is in scope here
I have seen some negative feedback on the way variables are scoped but, given that all scoping is entirely logical, I think it works very well.
There are already plans for a second preview version of this feature that will extend pattern matching to work with records and provide a simple way of implementing the deconstruction pattern. More details of this can be found in the JEP 375.
Anyone who’s written more than a few lines of Java code will have experienced a NullPointerException at some point. Failing to initialise an object reference (or mistakenly explicitly setting it to null) and then trying to use the reference will cause this exception to be thrown.
In simple cases, finding the cause of the problem is straightforward. If we try and run code like this:
public class NullTest {
List<String> list;
public NullTest() {
list.add("foo");
}
}
The error generated is:
Exception in thread "main" java.lang.NullPointerException
at jdk14.NullTest.<init>(NullTest.java:16)
at jdk14.Main.main(Main.java:15)
Since we’re referencing list on line 16, it’s evident that list is the culprit and we can quickly resolve the problem.
However, if we use chained references in a line like this:
a.b.c.d = 12;
When we run this, we might see an error like this:
Exception in thread "main" java.lang.NullPointerException
at Prog.main(Prog.java:5)
The problem is that we are unable to determine from this whether the exception is as a result of a being null, b being null or c being null. We either need to use a debugger from our IDE or change the code to separate the references onto different lines; neither of which is ideal.
In JDK 14, if we run the same code, we will see something like this:
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "c" because "a.b" is null
at Prog.main(Prog.java:5)
Immediately, we can see that a.b is the problem and set about correcting it. I’m sure that this will make many Java developers lives easier.
Now let’s turn our attention to the changes in the class libraries.
PrintStream has two new methods, write(byte[] buf) and writeBytes(byte[] buf). These effectively do the same thing, which is equivalent to write(buf, 0, buf.length). The reason for having two different methods is that write is defined to throw an IOException (but, bizarrely, never does), whereas writeBytes does not. The choice of which to use therefore depends on whether you want to surround the call with a try-catch block.
There is a new annotation type, Serial. This is intended to be used for compiler checks on Serialization. Specifically, annotations of this type should be applied to serialisation-related methods and fields in classes declared to be Serializable. (It is similar in some ways to the Override annotation).
The Class class has two methods for the new Record feature, isRecord() and getRecordComponents(). The getRecordComponents() method returns an array of RecordComponent objects. RecordComponent is a new class in the java.lang.reflect package with eleven methods for retrieving things such as the details of annotations and the generic type.
Record is a simple new class that overrides the equals, hashCode and toString methods of Object.
NullPointerException now overrides the getMessage method from Throwable as part of the helpful NullPointerExceptions feature.
The StrictMath class has six new methods that supplement the existing exact methods used when overflow errors need to be detected. The new methods are decrementExact, incrementExact and negateExact (all with two overloaded versions for int and long parameters).
The ElementType enumeration has a new constant for Records, RECORD_TYPE.
The MethodHandles.Lookup class has two new methods:
This is a new package in JDK 14 that has a single class, ObjectMethods. This is a low-level part of the records feature having a single method, bootstrap, which generates the Object equals, hashCode and toString methods.
The CompactNumberFormat class has a new constructor that adds an argument for pluralRules. This a String designating plural rules which associate the Count keyword, such as “one”, and the actual integer number. Its syntax is defined in Unicode Consortium’s Plural rules syntax.
The HashSet class has one new method, toArray, which returns an array, whose runtime component type is Object, containing all of the elements in this collection.
The LockSupport class has one new method, setCurrentBlocker. LockSupport provides the ability to park and unpark a thread (which does not suffer from the same problems as the deprecated Thread.suspend and Thread.resume methods). It is now possible to set the Object that will be returned by getBlocker. This can be useful when calling the no-argument park method from a non-public object.
The ElementKind enumeration has three new constants for the records and pattern matching for instanceof features, namely BINDING_VARIABLE, RECORD and RECORD_COMPONENT.
This package provides utilities to assist in the processing of program elements and types. With the addition of records, a new set of abstract and concrete classes have been added to support this feature. (Examples are AbstractTypeVisitor14, ElementScanner14 and TypeKindVisitor14).
One new method has been added to the ContentHandler interface of the SAX XML parser. The declaration method receives a notification of the XML declaration. In the case of the default implementation, this does nothing.
This is being introduced as an incubator module to allow testing by the broader Java community and feedback to be integrated before it becomes part of the Java SE standard. It is intended as a valid alternative to both sun.misc.Unsafe and java.io.MappedByteBuffer.
The foreign-memory access API introduces three main abstractions:
I scanned all 609 pages of the JVM specification but couldn’t find any highlighted differences. It will be interesting to look at the bytecodes generated for records since this doesn’t require any special support at the JVM level. The same applies to the helpful NullPointerException feature.
JDK 14 does include some JEPs that change non-functional parts of the JVM
There are a number of JEPs that relate to different parts of the OpenJDK:
#java #webdev