JavaWhat are Preview Features in Java 15?

What are Preview Features in Java 15?

Pattern Matching a Preview Features in Java 15

Preview features are Java language features that are nearly done, but not yet part of the official Java language specification. This mechanism was introduced around Java 12 to allow the JDK developers to push new functionality in the JDK without fully committing yes.

Once a language feature is part of the specification, it’s there to stay. Through preview features, developers can already try out new functionality and provide feedback to the JDK team. Then, when a feature is ready, it becomes part of the official Java language, just like what happens with text blocks, which we saw in the previous article. Java 15 contains three preview features.

We’re not going to cover these features in too much detail yet because they’re not officially part of the language and they might still change, but it will definitely give you a good feel for what’s coming in the next versions of Java. The first preview feature that we’re going to look at is called pattern matching, and pattern matching is actually a big feature that will be rolled out in many steps.

In Java 15, we can already preview the first part of this pattern matching feature. That is, pattern matching with instanceof based on types. Then we’ll have a look at records. Records are a much-anticipated feature introducing data-only classes in the Java platform. Both pattern matching and records have been previews in the previous version of Java as well. That’s not the case for the third preview feature that we’re going to look at, sealed classes. Sealed classes offer an improved way to control the inheritance hierarchy of a class.

But before we go there, we’re going to look at pattern matching. If you’ve seen the What’s New in Java 14 course, this feature will still look familiar to you, as there have been no changes in this preview feature. It’s also very likely that this feature will end up as an official Java language feature in Java 16. So why pattern matching using instanceof? You’ve probably all seen code like this where we first test whether an object is an instance of a particular type, BigDecimal in this case, and then inside the if we introduce a new local variable with the correct type, BigDecimal, cast the object, and only then start using it.

This idiom is familiar, but also quite repetitive. Pattern matching using instanceof provides a better way of doing exactly the same. In the abstract, a pattern is a combination of a predicate that can be applied to a target object and a set of binding variables that are extracted from the target, only if the predicate successfully applies to it. Now that sounds a bit abstract, so let’s apply it to this example. And here we can see that the type pattern, BigDecimal b, is indeed a test that can be applied through instanceof where we check whether the object is an instance of the BigDecimal type.

And it has a single binding variable, b, that will refer to the objects using the correct type if, and only if, the instanceof check succeeds. That means that inside the if we can start using the BigDecimal directly without casting or introducing an intermediate variable. And you might be wondering, what if we had an else branch for this if? Would the variable b also be in scope in this else branch? And the answer is no.

So the compiler has to be really smart about when and where the pattern variable is in scope because it depends on the pattern actually matching.

Another example, what if we would invert the check in the if where we say if this object is not an instance of BigDecimal b. In that case, this code would not compile because b would never be in scope because we know if the expression is out of the if evaluates to true, that object actually is not an instance of BigDecimal. So, even though the simple case of using pattern matching with instanceof looks indeed quite simple and can actually reduce the number of casts that you have in your code, it’s also good to know that things can get more complex once you start using pattern matching with instanceof in more complex expressions.

int value = switch (object) {
case BigDecimal b -> b.intValue();
case String s -> Integer.parseInt(s);
case Integer i -> i;
default ->
throw new IllegalArgumentException();
}

Assess when you use pattern matching with instanceof, this can definitely reduce the number of casts in your code. The difference is, of course, even more pronounced if you have code like this that has a series of these type checks and casts in succession. And this also brings us to a possible future direction for this feature because it’s entirely possible that we will see these type patterns in other places as well besides instanceof.

Here’s an example of how this feature might evolve where we have a switch expression, and in each case of the switch we have a type pattern with a binding variable, and then on the right-hand side of the case, we can immediately use the binding variable because it’s of the correct type. and it has matched on the right-hand side, so there’s no need to cast. Pattern matching using instanceof is really a first step on the long series of features that will introduce pattern matching into the Java language. If you want to learn more details about pattern matching using instanceof, you can read the Java enhancement proposal 305 at the URL shown here

Records a Preview Features in Java 15

Like pattern matching with instanceof, the records feature has also been previewed in Java 14 already. Nevertheless, because this is such an important and exciting feature, we’re going to look at it, and after giving a short introduction on this feature, we’ll focus on what has changed with respect to the previous incarnation of this feature in Java 14. But first, what are records, and why do we need them?

A normal Java class combines behavior or codes, that is, methods, with mutable states in fields of a class. And even though that’s the cornerstone of object oriented programming, sometimes you just want a class to represent some data without any fancy other stuff.

For example, if you want to have a data transfer object in your API or a class representing some database entity, you effectively just want to talk about data. And in those cases, writing a good class to do this is quite cumbersome. You have to create fields and getters, maybe even setters. You have to think about correct implementations for the equals, hashCode, and toString methods, and that’s a lot of work to just pass around some data. For example, a person with these three fields.

Now, you could use your IDE to generate a constructor, getters, setters, and equals, hashCode, and toString methods, but that has its downsides as well because you do it once and then people start modifying it, and moreover, you still need to read all this code and understand that the generated code only cares about passing some data. And remember, we read code much more often than we write it. Records offer a way out of this problem, and you can view records as a restricted form of a class that can be used to model data just as data without having to attend to all the concerns that we just discussed. And the cool thing is that creating such a record for this person example is now a one-liner in Java 15.

Rather than declaring a class, we can declare a record called Person. A record can have 0 or more components, and in this case, we have three components describing the data that we want to have as part of a Person.

In our example, a Person has a name, an age, and a hobby. Remember that we said that a record is a restricted form of a class, so there are some restrictions in place. For example, a record cannot extend another class or another record, although it can implement interfaces. Also, a record cannot have explicitly declared instance fields. All data of a record is defined through its components. With this one-liner, we declare a record and the compiler that takes care of the rest. It will ensure that there is a default constructor accepting all of the components so that the record can be instantiated, and implementation for the equals/hashCode/toString methods will be provided also based on the components of the records.

Furthermore, accessor methods for the components of the records will be provided as well. In this particular example, there will be a name, age, and hobby accessor method that will return the value of the given components. There are no setters because records are immutable. That means once you instantiate a record with given values, those values cannot change anymore. So, using this concise declaration of a record, you actually get a lot of functionality for free.

It is still possible to override the methods that are mentioned here, so the equals/hashCode/toString methods, as well as the accessor methods, if you want to. You can also provide an explicit constructor, for example, to do some custom validation. But all of this is optional. One major change to records as to how they were implemented in Java 14 is that you can now also have local records.

That means records that are defined inside of a method. Here we declare a record Point with two components inside of my method. This means that Point can only be used within this method. A local record is implicitly static. That means its body cannot refer to variables in the enclosing method or class. This is unlike local classes, which have been part of Java for a long time already. These are never static, and they can refer to variables in the enclosing scope. Not so for local records. To make records less of an exception, as of Java 15, you can now also declare interfaces and enums locally inside of a method. Just because you can doesn’t mean you should, of course.

Interfaces are all about APIs, and it does not make a ton of sense, usually, to declare one inside of a method only. For local records, it’s easier to see valid use cases, and we’ll look at an example of how local records can be useful in the next demo. If you want to read more in‑depth details about the records feature, then please have a look at the Java Enhancement Proposal 384. It contains every detail you might ever want to know about records. But since we’re interested in the big picture, we’ll skip much of the details and now go to a demo to see how local records work.

Example fo Local Records a Preview Features in Java 15

In this demo, we’re going to look at how we can create local records and how we can use this effectively. If you’d like to see a more general demo of records, then check out the demo in the What’s New in Java 14 course. I’ll be using the IntelliJ Community Edition again to show the demo. And if you want to follow along and want to try this out yourself, then make sure that in the module settings of your IntelliJ projects, you select the 15 (Preview) language level, which includes local records.

select the 15 (Preview) language level,

Otherwise, your code will not compile because it will not allow you to use preview features. So let’s head over to the IDE. What you see here is the definition of a normal non local record. In this example, an employee has an IDE, a name, and a level, which indicates their job level. Our goal in this demo will be to find the lowest paid employee. However, as you’ve seen, there’s no salary information inside of the employee records. We start our quest by setting up a stream of pre defined employees.

//https://onurdesk.com

public record Employee(String id, String name, int level) {
}

We’ll start with taking the employees stream that is passed as a parameter and then calling map. Now for each employee, we do want to find the salary to, in the end, determine who has the lowest salary. However, if you look at the return type of this method, you see that we need to return the employee and not the salary. So we cannot just blindly map the findSalary method over all the employees because that will use a stream of salaries rather than employees. So somehow we need to find a way to group the employee together with their salary. And when you encounter such a problem, you might be thinking of a tuple or a pair class that you could use to group the employee and the salary together

import java.util.Comparator;
import java.util.stream.Stream;

public class Main {
    public static void main(String... args) {
        Stream<Employee> employees =
                Stream.of(new Employee("1", "Mahesh", 1),
                          new Employee("2", "Suresh", 2));

        System.out.println(lowestPaidEmployee(employees));
    }

    public static Employee lowestPaidEmployee(Stream<Employee> employees) {
        record Salary(Employee employee, int amount) {}

        return employees
                .map(emp -> new Salary(emp, findSalary(emp)))
                .sorted(Comparator.comparing(Salary::amount))
                .map(Salary::employee)
                .findFirst()
                .orElseThrow();
    }

    public static int findSalary(Employee emp) {
        return switch (emp.level()) {
            case 1 -> 2500;
            case 2 -> 3000;
            default -> 2000;
        };
    }
}

We’ve probably all written a pair class or a tuple class as a utility in our code basis because there’s no such type in the Java standard library, but now with local records, we can do better. Instead, we can define a local record called Salary, which takes both the employee and an amount, and we can use this inside of the map to retain both the employee and the salary for the remainder of the stream pipeline. When you look at it like this, the records is like a named tuple where you can also refer to the components of the tuple by name, rather than using left and right or first component and second component and generic tuple or pair class.

This allows us to nicely express what’s actually happening in a very concise way. Let’s move back to the map. Now on the right hand side in the lambda, we’re going to instantiate a salary record, current employee, which is the first lambda parameter. And then for the second component, call findSalary with the given employee. After this map step, we now have a stream of salary records rather than employees where each salary record now contains both the employee and the associated salary.

Since we want to find the lowest paid employee, we can call sorted on the stream, passing a Comparator that is based on the amount of components of the records by using a method reference. Since we defined a local record with meaningful names, this is now very intention revealing code. We’re not completely done yet because what we want to return is an employee and not a salary record because this is local to our method implementation. So we predict out the employee again by calling map with another method reference to the accessor method employee of our salary records. And then we’re back again to a stream of employees, but now sorted on their salary. We can finalize this pipeline by calling findFirst, which returns an Optional.

And let’s just optimistically assume that there is always at least one employee, so we call orElseThrow to extract the employee from this optional and return it from this method. And as you can see here, we get back the employee, Mahesh, who was at level 1, which maps to a lower salary than Suresh, who was at level 2. In this demo, you saw how you can define a record inside of a method just as an implementation detail that makes it easier to implement your method. Often, you can use a local record where you would otherwise use a generic pair or tuple class from a utility library. By defining a local record with a meaningful name and explicitly named components, the resulting code becomes easier to read and to maintain. It’s now time to move to the last preview feature in Java 15 that we’re going to inspect, sealed classes.

Sealed Classes Preview Features in Java 15

In Java, unless you indicate otherwise, a class can always be extended. This is very powerful, but sometimes also not really desired. Sealed classes offer a way to control the inheritance hierarchy of a class in a more strict manner. You might, for example, want to model an exhaustive list of alternatives in a domain model by having various subtypes, but not more than the ones that you define. Or you would like to allow some subclasses to exist for your class, but you want to prevent unlimited extension maybe due to security concerns. At the moment, doing these things is quite hard to pull off in Java.

Let’s look at an example. Let’s say you wanted to implement an Option class, much like the Optional that we have in Java, that should only have two subclasses; the Some class indicating a value as presence, and an Empty class indicating an Empty option value. You want these types to be public so they can be used, yet no other subclasses of options should be allowed to exist. For example, the presence of a Maybe subclass where maybe there’s value, maybe there’s not, would be very unhelpful for the users of the Option API. The existence of such a new subtype, maybe coming from a different library or from user codes, makes it harder to reason about the option that was actually intended. So how can we prevent this?

How can we only allow Some and Empty as a subclass of Option, and disallow arbitrary auto subtypes? One tool to prevent subclassing is making a class final. However, that also means that we cannot create the Some and the Empty subclasses, which we want, so final is much too strict. Now, there is some way to achieve this without sealed classes, but it’s not really the prettiest. One pattern that you might have seen is creating an abstract class, in this case, the Option class, and then giving it a private constructor. In turn, we define the Some and the Empty subclasses as nested classes inside of the Option abstract base class. These nested classes have access to the private constructor, but any other class outside of this Option base class that tries to extend Option will not have access to the private constructor and, hence, will not compile. If this feels to you like a bit of workaround, that’s because it is. So it’s clear that the current tools we have to control the inheritance hierarchy in Java are pretty crude. Sealed classes offer a better way of modeling this.

Taking the same example, it works as follows. We can define the Option class, but in this case, with a sealed modifier, and at the end of the class decoration, we have a permits class. Permits instructs the compiler, but also in a JVM at runtime that the mentioned classes, in this case, Some and Empty, are the only allowed subclasses of Option. This makes it easier to reason about this type hierarchy. Only the types mentioned in the permits class can be a subclass. No other sometimes can appear, neither at compile time nor at runtime. Then we can declare an Empty class, which extends Option, and that’s allowed because Empty is part of the permits class.

And the same goes for the Some class. If you know a bit about type theory, you may recognize a sealed class hierarchy as a Some type; that is, a type that is defined by the Some of its alternatives, which are listed in the permits class. If you don’t recognize the term Some types, that’s fine, because you’ve now seen how they work anyway, and that’s the important part. Any other class that would try to extend Option would not compile. There’s a second way that you can use sealed classes as well. When you define a sealed class, like Option here, and in the same source file, you declare the subclasses, then you don’t need a permits class.

In this case, the compiler will infer that Empty and Some are the only permitted subtypes of option because everything is colocated in the same source file. Now, in the examples I’ve shown so far, all the subclasses of a sealed class have been declared as final ,so here you wouldn’t be able to extend Empty or Some. In this case, that might be the desired semantics, but there are other options as well. Let’s look at another example to understand these options. At the top left, we have a sealed class, Vehicle. This sealed class permits three subclasses, Boat, Plane, and Car.

What we’re interested in now is the possible class hierarchy beneath Car, Plane, and Boat. Let’s start with the Car class. It has been declared final, which means that there can be no other subclasses beneath Car. The Plane class is somewhat different. It has the new non‑sealed keywords, and non‑sealed, in this case, means anything goes, so there can be arbitrary subclasses of Plane. Note that these classes will also indirectly subclass Vehicle through the Plane class. However, it’s still not possible to declare a direct subclass of Vehicles because it has a permit class, which only contains boats, planes, and cars. The last option you have for a subclass of a sealed class is to again seal this class. We can see this with the Boat class. Boats, being a sealed class, also needs your permit class, indicating the allowed subclasses, which, in this case, are SpeedBoat and SteamBoat. If you’d like to learn more about the details of sealed classes, then read the Java Enhancement Proposal 360. We are now going to look at how we can use sealed classes to create a controlled hierarchy of classes in the next demo.

Sealed Classes Example

Let’s see how we can use sealed classes in practice. Before we get started, the same caveat applies as with the records demo. You need to enable the Java 15 preview language level in order for code using sealed classes to compile and run. Before we dive into the code, here’s what we’re going to do. We’re going to create a simple expression type hierarchy that can describe simple mathematical calculations. In this case, simple means it has constant values like just numbers, and we’re able to add them and multiply them together.

Therefore, our expression type will have three subtypes. To start, we have a simple interface Expr representing an arbitrary expression that in the end can be evaluated using eval, giving the result of the expression as an int. Now we can make this a sealed interface, so sealing doesn’t just apply to classes, but you can also use this for interfaces. Rather than introducing a Permits class, we’re just going to define the subtypes right here in the source file. Remember, we’re going to introduce three subtypes, one for a constant expression, one for an add expression, and one for a multiply expression. We could, of course, create a class that implements the expert interface, but we also learned about records. And even though records cannot extend any classes, in this case, we have a sealed interface, which the records can implement. So let’s try to create a record for the Constant expression.

public sealed interface Expr {
    int eval();
}

record Constant(int value) implements Expr {
    public int eval() {
        return value;
    }
}

record Add(Expr lval, Expr rval) implements Expr {
    public int eval() {
        return lval.eval() + rval.eval();
    }
}

record Mul(Expr lval, Expr rval) implements Expr {
    public int eval() {
        return lval.eval() * rval.eval();
    }
}

We declare a record with the name Constant, and it has a single component, the int value representing the Constant value. The record needs to implement the Expr interface, which means that we have to declare a public int eval method. Evaluating a constant is pretty simple. We just return the integer value that is part of the constant, and this gives us the first subtype of the sealed interface Expr. Note that we didn’t give a Permits class in the sealed interface declaration, but since we declared Constant in the same source file as the sealed interface, it all works out. Let’s move on to the add expression. Here, we can also use a record, but in this case, the record needs two components, the left‑hand side of the addition and the right‑hand side of the addition, which we call lval and rval. Again, this record is implementing the Expr interface, which means that we have to implement the eval method.

In addition, we first evaluate the lval and then add to the results the evaluation of the rval. After these two record decorations, you might have a question because we learned that every subtype of a sealed type must indicate that it’s either final, non‑sealed, or sealed to itself. And in this case, we don’t have any modifier under records. That is because records are implicitly final. You cannot extend the records. Because records are always final, we don’t have to be explicit about it. We’ll complete the example by adding a record Mul that, again, takes a lval and a rval and implements the Expr interface.

import java.util.Comparator;
import java.util.stream.Stream;

public class Main {
    public static void main(String... args) {
        Expr e = new Add(
                new Constant(1),
                new Mul(new Constant(7), new Constant(2))
        );
        System.out.println(e + " = " + e.eval());
    }
}

/*
// The following won't compile, because Expr is sealed:
record Negate(Expr value) implements Expr {
    public int eval() {
        return -value.eval();
    }
}
*/

The eval implementation looks similar to the previous one, but instead of adding, we’re going to multiply the two evaluations of the lval and the rval. And that’s it. We now have a sealed interface with three subtypes implemented by records, in this case, that we can use to construct and evaluate simple expressions. No other implementations of the Expr interface can be created outside of the source file. I’m going to open the Main class to show how we can use this type hierarchy. We’ll create an expression, e, which consists of a top level Add expression, which in turn takes two values, the left value and the right value, where the left value is the Constant of 1, and the right value is a Mul expression that has a Constant 7 as its left value and a Constant 2 as its right value. Because we declared the implementations of the Expr interfaces records, we get these constructors for free, which is pretty nice.

Now just pause for a little bit and think what the results of evaluating this expression e would be because that’s exactly what we’re going to do now. I’m going to print out the expression itself and also the result of evaluating this expression to the command line. Now when we run this code, we see on the left‑hand side a pretty printed form of the expression hierarchy that we just created. It looks so nice because of the automatically provided toString method of records, which shows the name of the record type and the values of all those components as key-value pairs. And then on the right‑hand side, you see 15, the result of evaluating our expression. And what better number to evaluate to than 15 for a Java 15 feature? Now there’s one last thing I want to show. Let’s say that we try to create a Negate expression as another implementation of the Expr interface. We can, again, declare any records that implement the Expr interface, and we can implement the eval methods, which just negates the result of evaluating the inner expression.

But as you can see, this code doesn’t compile. When we hover over Expr, which is higher than the rest, it shows the actual error, saying Negate is not allowed in the sealed hierarchy. And that’s because the declaration of our sealed Expr interface does not have a Permits class that includes Negate. In fact, the declaration of our sealed Expr interface doesn’t have any Permits class at all. This means that the Expr interface can only be implemented by types that are defined inside of the Expr.java file and not in Main.java as we tried to do here. This shows how you can use sealed types to prevent unwanted implementations or extensions of types that you declare and restrict them only to a subset the you control. If you want to experiment with what’s possible using sealed types and what is not, I encourage you to download this example from the exercise files and try to add some more implementations of the Expr interface yourself, maybe using a class rather than a record to see how the non‑sealed case would work or the final case. There’s a lot of room to explore this feature further if you want to. Now you’ve seen the three major preview features in Java 15. Let’s quickly review the key elements of these preview features and wrap up this module.

Summary of Preview Features

You’ve had a peek of the future of Java with three preview features in this module. The first one, pattern matching, is actually pretty close to release and will likely be available in Java 16. You can use type patterns together with instance of to prevent unnecessary casting in your code. What’s even nicer is that the pattern matching feature will keep expanding beyond this first step. Look forward to more pattern matching features in upcoming Java releases. Records offer a concise way to declare classes that just contain immutable data. By just declaring a name for records and listing its components, you get a constructor and correct implementation of hash code, equals, and toString for free, along with accessor methods for the components. As we saw in the demo, in the Java 15 preview version of records, you can now also declare records locally inside of a method.

Last, we looked at sealed classes. With this preview feature, you have more control over what subclasses are permitted for a sealed class that you declare. By explicitly listing permitted subclasses, it becomes easier to reason about the type hierarchy as you intended it. Preview features are good to get a feel for what’s coming in Java, but in the next module, we’ll look at Java 15 features in the JVM that you can actually use in production, so no need to wait for those to come out of preview.

LEAVE A REPLY

Please enter your comment!
Please enter your name here

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe Today

GET EXCLUSIVE FULL ACCESS TO PREMIUM CONTENT

Get unlimited access to our EXCLUSIVE Content and our archive of subscriber stories.

Exclusive content

Latest article

More article