BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations What's New in Java 16

What's New in Java 16

Bookmarks
29:11

Summary

Sander Mak discusses some of the new and preview features in Java 16: API updates, records, pattern matching, and sealed classes.

Bio

Sander Mak is Director of Technology at Picnic.

About the conference

InfoQ Live is a virtual event designed for you, the modern software practitioner. Take part in facilitated sessions with world-class practitioners. Hear from software leaders at our optional InfoQ Roundtables.

Transcript

Mak: My name is Sander Mak. I would like to guide you through some of the highlights that are delivered with Java 16. I've been active in the Java community for far over a decade. Currently, I'm part of Picnic in the tech leadership team of an online grocery scale-up. We're using a lot of Java. At the same time, I'm also very active in terms of knowledge sharing, for example, through conferences like these, but also on Pluralsight as an online e-learning platform. When Java 9 was released in 2017, I had the opportunity to work on a book together with Paul Bakker around the module system that is in there. This is not something that we'll be talking about, because in Java 16, the module system is still there, but it's not new anymore. We're going to focus on some other topics that are coming up in Java.

Outline

Java 16 itself was released in March of 2021. It is the first release for this year, and Java 17 will be coming in September. Java 17 will be the next long-term support release. I'm not going to go into what long term support means and why an LTS release feels and behaves different in the community, than the non-LTS releases like Java 16. Suffice to know that Java 16 has been released as a GA build, and can be used in production. That's why I wanted to highlight a few changes in Java 16. Most notably, we're going to look at both the stream API because there's a small delight in there that I would like to share with you. Other than the stream API update, I mostly want to focus on language changes. Language changes are actually delivered in Java 16, and some of the previous versions as well. In the next LTS release, in Java 17, you will have a lot of new features, if you compare it to, for example, the previous Java 11, LTS release, or even Java 8, if that's where you're still.

From Stream to List

Before we move to these language features, let's have a look at the stream API improvements that is delivered in Java 16. This piece, of course, should probably look pretty familiar to you if you're used to working with the streams API. We have here a stream of some strings. We map a function over it, then we filter. In the end, we want to materialize the stream into a list. Usually, you use the terminal operation collect and then parse in a collector. This collect, and then parsing in Collectors.toList is something that we see a lot. It feels a little bit boilerplate-y. The nice thing is that in Java 16, a new method was added to the stream API, and instead of doing a collects with this collectors.toList, we can now immediately call toList as terminal operation of a stream, which results in a list of strings that we get back, where we now only get the features that contain a space.

This list that we get back is an unmodifiable list. You cannot add or remove any elements anymore. If you want to collect into a mutable list, then you will still have to use a separate collector using the collect function. At the same time, you might wonder, are there any other to functions, to collect and to assess, for example? At the moment, at least there is only toList. In Java 16, we can use toList now rather than using collect, and then parsing in the collector to a list. Of course, there are many use cases where you may not want to collect to a list and you want to collect into some arbitrary other type, then you would still use the collectors. This is really just a small delight, and that will make a lot of the stream pipelines a little bit easier on the eyes.

Another method that has been added to the stream API, I won't go too much into because I want to spend more time on the language features. If you're interested in this, you should definitely also look up mapMulti, which is a new function. Its purpose is a bit similar to flatMap. If you work with flatMap and you map to inner streams in the Lambda that you parse there, mapMulti offers you an alternative way of doing this, where you push elements to a consumer. I won't go much into it now, but if you're interested and you work a lot with a stream API, then I definitely recommend looking also at the Java Doc in Java 16 for this mapMulti method.

Records

The first big language feature that was delivered in Java 16 is called records. Records are all about representing data as data in Java code rather than as arbitrary classes. Because, usually, when we have a class that simply should represent some data, we end up with something as shown here. Here we have a product class that has four members: name, vendor, price, and inStock. This should be all the information that we need to define. Of course, we need much more code to make this work. We need to have a constructor. We need to have getter methods to get the values of the members. To make it work correctly, we also need to have an equals/hashCode and toString implementation that are congruent with the fields that we defined. This can be generated by an IDE, that has drawbacks. You can use frameworks like Lombok, for example, that also has some drawbacks. In the end, what we're looking for is something in the Java language to more precisely describe this concept of having data only classes. That's what records are. We can say, public records rather than class, and then the name of the record type that we want to define here that will be products. Then we only have to provide the components that make up these records. Here we provide these four components by giving the types and the names, string name, string vendor, int price, and Boolean inStock. This is enough to define a record, which in the end is a special form of a class that only contains this data.

What does this offer us? Once we have such a record declaration, we will get a class that actually implicitly gets a constructor accepting all the values for the components of the record. We get automatically also an implementation for the equals/hashCode and toString methods based on all the records components. We also get accessor methods for every component that we have in the record. Here there will be a name method, a vendor method, a price method, and an inStock method that returns the actual values of the components of the records. Records are always immutable. You will see there are no setter methods. Once a record is instantiated with certain values, that is it, you cannot change it anymore. Also, records as a special form of classes are final. You cannot extend any other class when defining a record. You can implement an interface, so that's ok. You cannot create hierarchies of classes in records, or records in records. All in all, there are some restrictions here. It offers us a very powerful way to concisely define records consisting of components as a representation of only data with a given name in our applications.

How to Think About Records

It's maybe good to also set the mindsets in records. How should you think about these new language elements? It is a new and a restricted form of a class to model data as data. It's not possible to add any additional states to a record, as we'll see in the demo. It is really about modeling immutable data. You can also think of records as being tuples, but not just tuples in a generic sense that some other languages have where you have some arbitrary components, and you refer to them by index. Here, the tuple elements have actual names, and the tuple type itself, the record, also has a name, because names matter in Java.

How Not To Think About Records

There are also some ways that people might want to think about records that are not completely appropriate. First and foremost, they're not meant as a boilerplate reduction mechanism for any existing code you might have. Yes, we now have a very concise way of defining these records, but it doesn't mean that any data like class in your application can be easily replaced by records, because there are limitations. This is also not really the design goal. The design goal here is to have a good way to model data as data. It's also not a drop-in replacement for JavaBeans, because as we saw, the accessor methods, for example, do not adhere to the get standards that JavaBeans have. JavaBeans are generally also mutable, whereas records are immutable. Even though they serve a somewhat similar purpose, records do not replace JavaBeans in any meaningful way. You also should not think of records as value types that might also come in a Java language, where the value types are very much about the memory layouts and very efficient representation of data in classes. These two worlds might come together at some time, but for now, records are just a more concise way to express data only classes in Java.

Records Demo

Let's see what we can do with records. Here, I'm in IntelliJ, which already supports records, fortunately, and I've defined a product record. It has a name, a price, and an inStock flag. This definition that we have here is already enough to start using it. When I move to my Main class, I can already say, Product p1 is new product. Then we can use the constructor that is provided with all the record component. Let's say we have peanut butter, and it costs 200 cents, and it's indeed in stock. This is something that was generated for us based on the records definition that we have. If we look at the autocomplete, we can see that it also has the inStock, the name, and the price accessor methods for the values of the records. In this case, I'm just going to print the records to the console, so that you can see that it also has a very nice toString, where we can easily see the name of the record type with all the components and their values. This is also really convenient already.

If we're going to create another product, let's call it Product p2, just to be original, and we give it the exact same values. What we can see is that we can compare them by reference equality, but we can also call the equals method on the record that has been provided by the record implementation. What you will see here is that these two records are two different instances, so the reference comparison will evaluate to false. When we ask whether they are equal, it only looks at the values that we provide for the components of a record. In this case, it will say yes, p1 is equal to p2, because it's only about the data that's inside of the record. This way, you can see that the equality and also the hashCode implementation is fully based on the values that we provide to the constructor here for the record.

Going back to the record itself, again, here, we don't see any of these methods that we already use, because they're implicitly provided for us based on the record definition. You can still override any of the accessor methods or equals and the hashCode methods if you want to in this record definition. However, be aware that you preserve the semantics there. You do want to have a correct implementation of equals and hashCodes. HashCode with respect to the components of the record.

Another thing that you might want to do for a record is add additional methods. That's completely possible. You can also access the component values in these methods. Another thing that often comes up is validation. You only want to create a record if the input provided to the record constructor is valid. For that, we can also define a constructor here. We can say public Product, and then we have two choices. Either we can do a regular constructor, and then we provide all the component names again, as parameters to the constructor, or we can use a new format, so-called compact constructor, where we can leave off the formal parameter list. Implicitly, we will have access to name, price, and inStock here. We can, for example, say if price is less than zero, let's throw a new IllegalArgumentException. This way, when we instantiate a product with a price below zero, it will not happen, it will just throw an exception. If the price is above zero, we don't see any assignments here to the name, price, and inStock. These are added implicitly, again, by the compiler when compiling this record. This is a very interesting mechanism.

We can even do normalization if we want to. We can say, for example, price is maybe some default price like 100. Then this 100 will be assigned to the price variable to the constructor here, which is implicitly available. Then, again, the assignments to actual members of the record, so the final fields that are part of the record definition that was generated, are inserted automatically by the compiler at the end of this compact constructor. All in all, a very versatile and very nice way to define data classes in Java. There's one last thing I want to show here, because we now defined a top level record, but you can also declare and define records locally in methods, which can be very handy if you have some intermediate state that you want to use inside of your method.

We can, for example, say that we want to define a discounted product where we define this as something that takes another product. The record type that we already have we use as one of the components, and we add a Boolean saying whether it's discounted or not. We won't provide a body here, the antibody is all right. Let's say that we want to print such a discounted product.

There, again, we can instantiate it, just like this, DiscountedProduct, providing p1 as the product here. Let's say that it's indeed discounted. Let's run this. You will see that this behaves exactly the same as the top level records that we declared in the product source file. Records as a local construct can also be very useful in situations where you want to group some data in maybe an intermediate stage of your stream pipeline. There are many other use cases as well.

Where Would You Use Records?

Speaking of use cases, when we think about records, where would you start using them? I think there are some obvious places where records will be used a lot. One of them will be DTO, so Data Transfer Objects, which are the definition of objects that don't really have any identity or behavior, but are all about just transferring data. For example, the Jackson library as of version 2.12 already supports serializing and deserializing records to JSON and other formats that are supported. I think there, records will really be helpful. Now, JPA, so the Java Persistent Architecture is not a place where people think they might want to use records. If you want to use it for entities, for example, that's not really possible, because it is heavily based on the JavaBeans convention, usually, these are mutable rather than immutable. I don't think they will be used there a lot. There might be some opportunities when you instantiate view objects in queries, for example, where you could use records instead of regular classes. For entities, not so much.

I think records will also be very useful when you want to have a map key that consists of multiple values that you want to group together. Using records, you get automatically the same behavior for equals and hashCode. There, too, records will be very useful. Since records can also be thought of as nominal tuples, a tuple where each component has a name, you can easily see that it will be very convenient to use records to return multiple values from a method to the caller. There, also, records can be very handy. All in all, I think it's a very exciting development that we now have records in Java. I think they will see widespread use.

Pattern Matching for instanceof

This brings us to the second language change in Java 16, and that is pattern matching for instanceof. This is a first step in a long journey of bringing pattern matching to Java. We'll see some examples of that. For now, I think it's already really nice that we have this in the language in Java 16. You will probably recognize this pattern, where some piece of code checks whether some object is an instanceof type, in this case, string. If that's the case, then inside of the if, we need to declare a new variable, we need to cast o, in this case, to this new string s, and only then we can start using s as a string. This works, but it is not really nice, and it's not really intention revealing code. Can we do better? As of Java 16, yes, we can using the pattern matching for instanceof feature. What we can do is instead of saying o is instanceof a type, we can say o is instanceof a type pattern, where a type pattern consists of the type and a binding variable, in this case, s.

What happens here is that if indeed o is an instanceof string, then s will be immediately bound to the value of o, given the correct type, which means that we can immediately in the body of this if, start using s as a string without having to cast. The other nice thing here is that the scope of s is limited to just the body of this if. One thing to note here is that the aesthetic type of o in source code should not be a subtype of string, because in that case, it will always be true. If the compiler detects that, then it will be a compile time error. Here, we see that this neat pattern abstracts away a lot of boilerplate from the previous idiom that we saw.

The interesting thing here is, what would for example happen if we, instead of having the instanceof check, have the negation, would then s also be in scope for if? What we see here is that the compiler is actually really smart about this, because it sees that if this check evaluates in the end to true, then in the else branch, we would have s in scope with the type of string, whereas in the regular if branch, so the non-else branch, s would not be in scope, we would only have o in scope. We should do something else there like returning zero in this case. This mechanism is called flow scoping where the variable, the type pattern variable is only in scope if the pattern actually matches and the match succeeds. This is really convenient. It really helps tighten up this code. It is something that you need to be aware of and might take a little bit of getting used to.

One example where you can very nicely see this flow typing in action as well is in this particular implementation of the equals method that you might also recognize, where you immediately return a first o instance of MyClass, which is the regular instanceof check. Then combined with the And operator, where you cast o to MyClass, then immediately get a name and call equals on that. This can be simplified using pattern matching for instanceof to directly use o instanceof MyClass m. Then because of flow typing in the right-hand side of this And operator, we don't have to cast anymore, we can immediately use m as being of type MyClass. Again, a nice simplification and a nice way of cleaning up this previous casting that we had in the code.

Pattern Matching: Future

What we see here with pattern matching for instanceof is only a small step into a journey that adds a lot more pattern matching in Java. Some of the future directions that I'm showing here have been sketched out by the Java team. There are no promises on when or how this will actually end up in the official language. These are just some ways that the language might evolve to implement pattern matching in other features as well. Here, we can see that in the new switch expression, which we already got in an earlier version of Java, we can now also use type patterns, like we saw with the instanceof for our cases. We have case integer i, and if that matches, then on the right-hand side, after the arrow, i is actually an integer. We don't have to cast it anymore, because the pattern has already matched, and bound the Object o as an integer to the variable i. The same for all the other cases that we see here.

It's not just about taking type patterns and applying them elsewhere. It's also about introducing new kinds of patterns that you can match against in the language. We already saw records. We saw how you could construct records. Here, you see a really new and exciting direction, where we might be able to also pattern match our records, and immediately binds to the component values with new variable names. Here we have a Point record with x and y. If the Object o is indeed a point, we will immediately bind the x and the y components to the x and y variables here, and we can start immediately using them. If it's not the case, then we could just skip over the if, and then move on. That's a really nice future direction for pattern matching as well.

There are more different types of patterns that we might anticipate here, so array patterns can be another one where you match on o again. If o is an array of strings, in this case, you immediately extract the first and the second part of the string array to s1 and s2. This, of course only works if there are actually two or more elements in the string array. We just ignore the remainder with these three dots. Again, this is not in Java 16. This might be coming in a future version of Java. It shows that this pattern matching for instanceof, which in itself is just a nice, small feature, of course, is a small step towards this new future where we have lots more places where we can do pattern matching, where we also have additional kinds of patterns that we can match against, besides the type patterns that we saw.

Preview Feature: Sealed Class

I want to briefly tell you about the sealed classes feature. This is a preview feature in Java 16, which means that you need to add the enable preview flag to your compiler invocation and your JVM invocation if you want to use this feature. It's not final yet. However, it is meant to control your inheritance hierarchy. Let's say you want to model a super type Option where you only want to have Some and Empty as subtypes. What you want to prevent here is that somebody arbitrarily extends your Option to maybe include a Maybe type so that you have an exhaustive overview of all subtypes that exists of Option of which Maybe is not part. The only tool to control inheritance in Java at the moment is final. This means that there cannot be any subclasses at all. There are some workarounds to be able to model this feature without sealed classes, but using sealed classes, this becomes much easier. What we can do there is we can define the Option class to be sealed. Then, after the class declaration, we have a permits keywords. Then we say only Some and Empty are allowed to extend class Option. We can just normally define Empty and Some as classes. We of course want to make them final there because we don't want any other classes to extend Empty and Some, but no other class can now be compiled to extend Option. This is enforced by the compiler through the sealed classes mechanism. We now indicate that only Some and Empty are allowed subtypes of Option.

We can also do this without a permit class if we define all of these classes in the same source file. An example here is that we say your Option class again is sealed, but instead of having separate source files, and indicating using permits that Empty and Some are allowed to extend Option, all subclasses in the source file are allowed to extend Option. We can define everything in one place and in one go. This effectively is the same as we saw before with separate source files but now without the permits keywords, and just the sealed keywords on Option. Again, there's lots more to say about this feature. If you're interested in that, I highly recommend going to the JEP page, the Java Enhancement Proposal for this feature, JEP 360, and read more about it. This is something that is not officially part of Java 16. It will be part of a future Java release as a real language feature. Already worth looking into.

What Else?

There are a lot of other things in Java 16 that we could not cover in this session. There have been newly added APIs, for example, the vector API, and foreign linking and foreign-memory-access APIs, all very interesting. We focused here on the language features rather than on the API additions. Also, at the JVM level, a lot of improvements have been made, for example, ZGC, has had some performance improvements. We have Elastic Metaspace improvements around how this is handled by the JVM. Also, very impactful is that now, encapsulated types in the JDK will be really strongly guarded even when running your application from the classpath.

Resources

Again, if you go to the JEP overview page in the link, openjdk.java.net/projects/jdk/16/, you will find a lot more detail about all of these changes. I highly encourage you to look into those because a lot of these are really interesting. Some of them can even be very impactful to your applications, as well. As the last highlight in Java 16, there's also a new packaging tool for Java applications introduced, which allows you to create native installers and packages for Windows, Mac, and Linux, also very useful. There's also a JEP for that. I encourage you to also look at that if that's interesting to the projects that you're working on.

If you're interested in more, I have a lot of content at Pluralsight. If you follow the link, bit.ly/ps-sander, you will see an overview of a lot of what's new in Java courses. In April 2021, currently, they're also free to watch. I also highly encourage you to go there and check that out if you're interested in more details about what's happening in Java.

 

See more presentations with transcripts

 

Recorded at:

Jun 13, 2021

BT