Introducing Java New Features — From Java 8 to 17

Rajat Rastogi
5 min readOct 8, 2023

Java language has undergone several changes and modifications since Java 1.0. Java 17, has some considerable changes to look at and take advantage of, since the most popular Java 8(1.8). This blog is a take on the new and significant features added or bundled up (since past releases), with an aim to know the new features and benefits, thus take informed decisions and promote better coding practices.

Features

1. Record Classes

First previewed in Java SE 14, this feature is permanent in Java SE 16. A record declaration specifies in a header a description of its contents; the appropriate constructors, accessors, equals, hashCode and toString methods are created automatically. A record’s fields are final because the class is intended to serve as a simple “data carrier”.

record Shape (String name) {};

We just specified a single private field name. And all things for a basic class definition are taken care of.

public class Java17Application {

public static void main(String[] args) {
System.out.println(new Shape("My Shape").toString());
}

}

The output of the above run is as below. Thus it produces a basic shape class where we leverage the constructor and toString method call.

Shape[name=My Shape]

One thing to note about Records is that these do not have traditional getters and setters. They are immutable and final and hence we cannot modify the field values once set.

These were a few notable things about records. It does provide a less code approach towards a lot of simple data classes but it also abstracts a lot of information, thus it sparks a similar debate on trade-offs as using popular Lombok.

Let’s swiftly move towards another new advantage to which Records also contribute, apart from saving us from writing code.

2. Improvements in Deserialization

Before understanding the enhancement and improvements let’s understand how serialization and deserialization work in Java.

Serialization: Java serialization uses reflection to scrape all necessary data from the object’s fields, including private and final fields. If a field contains an object, that object is serialized recursively. Even though we might have getters and setters, these functions are not used when serializing an object in Java.

Deserialization: When deserializing a byte stream back to an object it does not use the constructor. It simply creates an empty object and uses reflection to write the data to the fields. Just like with serialization, private and final fields are also included. Not using a constructor means any checks done as part of the constructor are totally skipped. This gives rise to vulnerability when a malicious user modifies a serialized object before it is deserialized like making the start date after the end date or making any arbitrary code execution possible using Gadget Chains (A gadget chain is a chain of function calls from a source method, generally readObject, to a sink method which will perform dangerous actions like calling the exec method of the Java runtime).

To mitigate such vulnerabilities Java came up with Record Classes(discussed above) which unlike other classes are deserialized by calling the constructor method, thus all safety checks are executed. The deserialization filters were introduced, using these, we can place limits on array sizes, graph depth, total references, and stream size. In addition, we can create blocks and allow lists based on a pattern to limit the classes we want to get deserialized. These can be set up at the global level (JVM) or per stream.

Specifically talking about Java 17, this brings improvements in the deserialization process further by avoiding overriding of global filters whenever a stream level filter is set. It gives more powers to merge or combine and use a number of filters that are more context-aware.

This new Java version now supports a specific event to monitor deserialization. A deserialization event will be created for every Object in a stream and records all sorts of interesting things like the actual type, if there was a filter, if the object was filtered, the object depth, the number of references, etc.

3. Sealed Classes and Interfaces

First previewed in Java SE 15, this feature is permanent in the Java SE 17 release. Sealed classes and interfaces restrict which other classes or interfaces may extend or implement them. The most common use case to restrict inheritance could be for a library where we want to control the re-usability of code in one of the classes by not allowing any arbitrary client class to inherit from it. This is achievable by sealing the parent and permitting inheritance by a restricted set of classes. Let’s see the same example to make the shape a sealed class and permit only Circles, Square, and Rectangle to inherit from it.

public sealed class Shape
permits Circle, Square, Rectangle {
}

Each of the subclasses has to be declared as sealed, non-sealed, or final. Point to note for Record Classes — These cannot extend any class hence sealed classes + Record classes don’t work but these can implement Sealed Interfaces. Record class is always final.

public sealed class Rectangle extends Shape permits FilledRectangle {
public double length, width;
}

4. Text Blocks

First previewed in Java SE 13, this feature is permanent in Java SE 15. A text block provides clarity by way of minimizing the Java syntax required to render a string that spans multiple lines.

In earlier releases of the JDK, embedding multi-line code snippets required a tangled mess of explicit line terminators, string concatenations, and delimiters. Text blocks eliminate most of these obstructions, allowing us to embed code snippets and text sequences more or less as-is. Thus promoting easy readability. A text block is an alternative form of Java string representation that can be used anywhere a traditional double-quoted string literal can be used.

// ORIGINAL
String message = "'The time has come,' the Walrus said,\n" +
"'To talk of many things:\n" +
"Of shoes -- and ships -- and sealing-wax --\n" +
"Of cabbages -- and kings --\n" +
"And why the sea is boiling hot --\n" +
"And whether pigs have wings.'\n";

The above is a sample of how we used to concatenate a multi-line string, below is the new way of doing the same.

// BETTER
String message = """
'The time has come,' the Walrus said,
'To talk of many things:
Of shoes -- and ships -- and sealing-wax --
Of cabbages -- and kings --
And why the sea is boiling hot --
And whether pigs have wings.'
""";

5. Local variable Type Reference

Introduced in Java SE 10. In the Java 11 release, it has been enhanced with support for allowing var to be used when declaring the formal parameters of implicitly typed lambda expressions. In JDK 10 and later, we can declare local variables with non-null initializers with the var identifier, which can help us write code that’s easier to read. Var is a reserved type name and not a reserved keyword hence variables and packages declared as var in earlier written code do not break but class or interface names might have to be renamed. This saves us from writing extra code when the type can easily be known from the right-hand side of the initialization expression(for example). Sometimes we need to balance extra code vs informative code and hence decide on when to use var and when to avoid it. We can use var in for loops and also from Java 11 onwards in the Lambda expression params.

var list = new ArrayList<String>();    // infers ArrayList<String>
var stream = list.stream(); // infers Stream<String>
var path = Paths.get(fileName); // infers Path
var bytes = Files.readAllBytes(path); // infers bytes[]

--

--