BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Presentations Records and Sealed Types - Coming Soon to a JVM Near You!

Records and Sealed Types - Coming Soon to a JVM Near You!

Bookmarks
53:12

Summary

Ben Evans talks about the recent changes in the Java language (Records, Sealed Types, Pattern Matching , etc.) and shows how big ideas in language design sometimes start from surprisingly small implementation details.

Bio

Ben Evans is Principal Engineer and Architect for JVM technologies at New Relic. Prior to joining New Relic, he co-founded jClarity (acquired by Microsoft) and was Chief Architect (Listed Derivatives) at Deutsche Bank. He is the author of 5 books - "The Well-Grounded Java Developer","Java: The Legend", "Optimizing Java" and "Java in a Nutshell". He is the track lead for Java / JVM at InfoQ.

About the conference

Software is changing the world. QCon empowers software development by facilitating the spread of knowledge and innovation in the developer community. A practitioner-driven conference, QCon is designed for technical team leads, architects, engineering directors, and project managers who influence innovation in their teams.

Transcript

Evans: My name is Ben Evans, I use he/him pronouns. Because I work for a public company, I have to show you this slide. What it means is despite the fact that I don't work for Oracle and don't even have a commitment on OpenJDK, you shouldn't take anything I'm about to say about the future of Java at all seriously, especially if you're going to make financial decisions based upon it, which I'm sure many of you will.

For those of you who have not met me before, I'm principal engineer and architect for JVM technologies at New Relic based in sunny Barcelona, which is lovely. Before that, I co-founded a company called jClarity with the aforementioned Martijn Verburg, which was based out of the London Java Community originally, and last year, we sold it to Microsoft. That was quite an eventful few months. Before jClarity, I was chief architect for Listed Derivatives at Deutsche Bank, and before that, at Morgan Stanley, where I did a number of things, including the Google IPO.

I'm also known for some of my work in the community. I'm a Java Champion, if you know what that is. I'm JavaOne Rock Star speaker, which I probably should rename given that JavaOne hasn't existed for a number of years now and younger people in the audience might not even know what it is, so I should probably change that part of my interest slide. I also served on the Java Community Process Executive Committee, which is the body that makes all your Java standards, for six years. Then, during my time living in London, I was part of the organizing team for the London Java Community and co-founded a project that you might have heard of called AdoptOpenJDK.

Why Enums?

I want to talk about something which I know is very dear to everyone's hearts. I'm talking, of course, about enums. Now everyone's looking at me, and they're thinking, "Why on earth has Ben [Evans] just said the word enums six times in a row? We're talking about records and sealed types, aren't we?" Well, we are. Why enums? There's a general principle and there's a specific principle, and I'm going to talk about the general principle first.

The general principle is part of language evolution, is that patters in one language become language features in languages that follow. Think about that. A pattern, as we all know, especially in object-oriented language, is the idea that you have a small group of classes that somehow operate to provide a reusable and recognizable language construct. My thesis is that, over time, languages, which obviously belong to a community of languages that existed beforehand, are able to take things which were encoded as patterns and turn them into part of the language core.

What do I mean? That seems like a pretty bold statement that I might have to justify. I mean things like vtables in C becoming the virtual keyword in C++. Hands up if you've got C++ programmers in the room, anyone knows some C++? A few people here know what I'm talking about, the idea of a table of function pointers turning into the virtual keyword. Of course, in Java, we're taking this one step further. We don't have a virtual keyword anymore. Virtual has become so much part of the language landscape that it actually disappears from view, which is the stage of evolution which is beyond the one that I'm talking about here. From a pattern to a language feature to being just so much part of the woodwork and so much part of the background that you cease to even see it as a feature, never mind the pattern anymore.

The iterator pattern in C++ becomes the iterator interface in Java. If you have read the old Gang of Four patterns book, you'll find patterns like iterator in there, and for younger people, you look at it with fresh eyes and you think, "Why are we talking about this? Why is this a pattern? It's just a thing. It's just a part of the library. It's just become part of the landscape." Of course, you knew I'll get back to enums. The enum approach in C++, which, of course, is just a horrible hack over integers, in Java, there's quite a bit more to it than that. That's the general principle. I could list out a bunch of others, but I thought that those were the closest to the surface to try to, at least, provide some sort of anecdotal justification for the point that I'm trying to impress upon you here.

Then, of course, there's actually the specifics about Java's enums individually. Java enums are a restricted form of class. They have semantics that are defined by a pattern. That pattern we might call finitely many instances. Notice the way that I've framed this. The semantics are defined by a pattern. From the pattern comes compact syntax, and it's that way around. The semantics are defined by the pattern, the syntax comes from the semantics.

Demo – Decompile an Enum

Now, let's actually go to my first demo of the day. I have a simple Color enum, pretty much the simplest thing you can think of when doing an enum, and because I have to work with multiple JDKs, let's just use Java 14. Now, let's just javac. There's the javap tool to disassemble it.

What have we got? We can see that each of the individual names have been turned into public static final fields, each of the correct type. We have a thing called values, which gets back this field $VALUES, which has an array of color in it. That's actually a private field, so it doesn't show up in this particular view, but then we clone that to return this as a value field. There's an ancient region in there for why you do it that way, to do a serialization, but I shan't trouble you with that. You also have a Color valueOf which you use to look up this. Notice that there's a parse called java.lang.Enum up here, which every enum extends. Now, if you go and try and extend this class directly, the compiler won't let you. Then we've got this static initializer block down here, which sets up and basically initializes each of the enum constants, which now exists as public static fields.

That's a lot of generated code, a lot of stuff which has been provided by that many characters. Decompiling it actually shows us that we already have language constructs in Java where stuff is being auto-generated for us by the compiler. When I come on to show you some things with records, in particular, this is not a new idea that we have the compiler doing significant work for us. Ok, just going to pause there. That probably is enough about enums for right now, although, spoiler, I'll be coming back to talk about them in a bit.

Project Amber

Now, I'll introduce something you might not have been aware of in OpenJDK, which is this thing, which is Project Amber. Project Amber is one of the projects which has been happening in OpenJDK to try to explore research directions, and there are several of these things out there. The one which you might have heard more than Amber is Project Valhalla. Everyone's heard about inline types and inline classes, and that's kind of the big bombastic boil the ocean, change the entire world project.

The goals of Project Amber are rather more modest, or so it, at first, appears. Project Amber is about trying to find smaller, in some sense, features which are about productivity and which are core to the Java language itself. There's very little VM level of change necessary for these things. There's a bit of stuff in the compiler, there's a bit of stuff in the class file format, but very little in the type system and almost nothing in the VM. That means that you can try to deliver your large goal by breaking it apart into smaller pieces. If you build one feature on top of the other in the right way, you might get to some quite surprising basis. The old saying about you might grow a mighty oak from a tiny acorn, or in this case, a sequence of delivered tiny acorns which are part of a concerted set of deliveries to build up some much larger ideas. That's Project Amber.

Why Records?

Let's talk about records. What are they? This is by analogy with enums, and you'll hopefully start to see now why I spent the first 10 to 15 minutes talking about enums. We want a first-class support for modeling a data-only aggregate. There's a pattern here. The pattern is defining some semantics for us. The pattern is called the state, the whole state, and nothing but the state, which sounds remarkably totalitarian, so maybe we should find a better name for it. We could call it the data carrier pattern. What it means is that there is a subsetting of classes, a reduced form of the class where we want to make it clear that nothing really happens except that the instances of the class are totally and completely defined by the state that they carry and they have no real further semantics or further behavior other than that.

It turns out that this also closes a rather annoying gap in Java's type system, which we've had since the very beginning. Then, notice that our next goal is to provide a language-level syntax for this pattern that we came from up here. Finally, notice the bits at the bottom of this list, and this is in descending order of importance. It reduces the class boilerplate. This is actually a Java example of a very important principle in programming language design.

Wadler's Law

Wadler's law has this idea, emotional intensity of debate on a language feature increases, so the debate gets more intense and more vicious, more unpleasant, the further down this list you go: semantics, then syntax, then lexical syntax, then comments. Given that we're Java people, I might also put in a new entry here between lexical syntax and comments, multiline strings, a feature which is effectively trivial but yet which has attracted more debate than anything that I can think of recently. Because I was around for the Java 7 days, I would also put strings and switch here as well.

Wadler's point here is that the reason why this happens, the reason why people debate these things more and more as you go down the scale, is because there are more and more people who feel competent enough to have an opinion about them. At the very highest levels of the language, there are very few people who can coherently argue about what major language semantics should look like. Everybody has an opinion about what comments should look like. The other term for this is called bikeshedding. If you've ever heard of the idea of bikeshedding, which really came from the operating system community, whereas, in the programming language community, we like to call it Wadler's law instead.

Boilerplate

Because we want to talk about boilerplate, let's talk about boilerplate. It's nice and accessible, all the stuff you hate to type, all the things you don't want to, the toStrings, the hashCode, the equals, the Getters, public constructors the list goes on. What do you do? There's really two things you can do. You either get a new ID to generate them. Hands up if you do that. Keep your hand up if you have had a bug in production which caused an outage by not keeping these things up to date by regenerating them when you know you should have. Yes, I thought so. There always are. Then, you might think, "Let's solve this problem in another way. Let's have Lombok." The laughs in the back of the room tell me that there are also people in here who've been bitten by Lombok. Lombok is really not a good solution for this, because it does some really terrible things in the way that it's implemented. It's extremely clever, but the older you get and the more senior hopefully you get a programmer, the less in love you are with clever solutions.

We need something new. If we're going to get rid of the boilerplate, how are we going to do it? Let's start with the Java Cashflow class, like this. We set it up, and notice how we've got this array for a public constructor. Notice that we've got an awful lot of repetition here. We have a currency and a field called currency, so parameter, field, method, all of which are doing the same thing. They are referring to the same fundamental thing. What we have is a pattern. We are doing a lot of typing here to represent the pattern that this thing is just these things, and they access like this, and there is no other way of doing it, and there is no more semantics other than those fields.

Fortunately, I found some help. What we get is this, this legal Java 14 syntax in preview mode, and we have got rid of all of the boilerplate, but that's not the important thing. Remember Wadler's law, the important thing is what we're saying is that those three things are everything which matches about one of those. That is nothing more than the sum of these parts or maybe I should say the product of these parts. What are they?

What Could Records Be?

If we're concerned with semantics, what are our semantics here? This is kind of backwards. I'm showing you the answer before I've really shown you the question. When Brian and the others on the expert group were designing it, this was the question they started with. In the design space, what are these things? What could a record be? They come up with really four possible alternatives. They could be boilerplate reduction. Yes, they could be, and for some people, that might have been the right answer, but it does kind of violate Wadler's law principle. They could be Java Beans. Hands up if you are really hoping these were going to be Java Beans. Only a few people or at least only a few people would prefer to admit it in front of the room. Obviously, as you can probably guess the way that I've set this up, they're not Java Beans. They don't have setters, and the naming convention for the getter methods on the previous slides, you may well have noticed, was not a Java Bean convention.

They could be these things called product types as a form of algebraic data type. Or they could be something in the middle, a named tuple, which is kind of similar to a product type but it's got a name. If you've been around Java for a while, you will know that, actually, we tend to like names in Java. We're very sure that everything in our language, every type has a name. Even if we can elide it or get rid of it or not have to talk about it explicitly, as we do with lambda expressions, deep buried in the heart of it, there actually is a name involved. You probably won't be surprised if I tell that, actually, records are named tuples. They have names. They are not structural typing. They emphasize the semantics above everything else. They emphasize the fact that it is simply a collection of fields and nothing more elaborate than that.

There is a tight binding between what you call the parameters versus what you call fields versus what you call the access of methods, and that's just the way that we do it. This is not to be a complete replacement for everything that classes do. If this pattern doesn't fit with what you're doing, don't use it. It's there for those cases which people believe are very common where this is what you do want. One of the things that I found about it as I've been working with them is that it's actually very natural. You get a sense quite early on when working and building with records, "This thing has other semantics. It's not just a plain collection of fields. It, therefore, needs to be a fully-fledged class," and that's ok.

Demo – Working With Records

Let's show the second demo. I started my career in finance, as you saw from my opening slide. At Deutsche Bank, before I was involved in Listed Derivatives, I was actually in foreign exchange. I secretly have a bit of a soft spot for FX applications. This is not to be taken seriously as an FX application. I would not put this into production. I'm not claiming that I would. It's assumed to be open-sourced application which basically is just designed to show off Java 14 features just as a reference application. Once it's open-sourced, if you actually bug request and bug report about how terrible my matching engine code is, I'll be very happy to accept them.

The way it's laid out is called foxy, and under the foxy packages, we have a couple of things. We have the domain model, we have an engine, and then we have a very simple Jetty Handler, which is just there for status checking and making sure that Kubernetes doesn't kill it and stuff like that. I'm going to focus, first of all, on the domain. I've got a bunch of enums, currency pairs, which are going to be what I'm going to trade We've got some currency pairs enums. This is actually a fully-fledged record. If you look just very carefully at the top left of the screen up here, this is the early access preview of IntelliJ. This is actually able to cope with previous and 14 and so forth.

I define my record to be an FXOrder telling number of units, current scale trading, the side, a price, sentAt, time to live, and because I want to do everything immutably, because records are immutable, I want to chain the order to say, "This one came from another order originally," so that if I trade and something partly matches one order, I can link it back to an original order that was originally sent.

That's the structure of my data. You'll notice something quite interesting here, which is, down here, I've got some static factory methods, and I've found that this is a pattern for working with records that works really well, is rather than having multiple constructors – and this is very much an emerging pattern I'm quite happy to be shut down about this once we've actually got a bit of experience working with records – but I find static factory and a single canonical constructor is actually a better way to work with these. In these, all that happens is these just basically fill in some parameters, because, of course, we don't have before parameters in Java, and then they create new objects.

The thing I want to draw your attention to is this thing down here. Look, it just fits on one slide. In a class declaration, you say public class, name of class, curly braces, then you say constructor, and the parameters live on the constructor declaration. With records, our parameter declarations live on the record declaration itself. In the duality of syntax, the constructor does not need to repeat that list of parameters. This is why I think the canonical constructor approach works well. Because I know what the parameters are, because they're the parameters of the record itself. If I want to do anything special in the constructor, I don't have to repeat myself. I can actually just say, "This thing is the constructor, and I do all these things in it." Basically, it's very straightforward. It's simply some states checking. I want to make sure that any time I create one of these order objects, it's completely set up the way I want, and any misbehaving things like time to pass in nulls to the enum types, we'll just throw in an argument exception.

This is actually called a compact constructor, because it doesn't have full declaration and because I don't actually say to set all the parameters. I don't need to. It's taken as read. In these constructors, at the very end, if you've passed all the checking, you can set all of these fields. That will be automatically generated in the compiler as well. The units, the currency pair, the side, all of the other parameters will be set invisibly down here. This, by the way, shows us one way in which these are better than structural or shape-based tuples. I can do this kind of error checking, because I actually have a constructor to hang them off. If all I was doing was taking things and putting them together into a round bracketed tuple of multi-values, there would be nowhere for this to actually run. One of the key advantages of having the named approach is to do it this way.

That's the FXOrder object. We've got a couple others as well. I've got an FXResponse. I've got this sad little code comment up here which says, "This interface really wants to be a sealed interface, but there are no sealed types yet." Instead, what I've had to do is have an interface called FXResponse. You can have records to implement interfaces, but this is the design sense. It's actually difficult to build out of, because how much further down this route you want to go before this thing is genuinely a class is a separate question. In this case, I'm using FXResponse purely as extra type information, and if you notice, it was actually a marker interface. I think that this pattern would be more common if we did have sealed types in play as well.

I'm going to show you one other thing which happens, which is actually not in the domain package, but eventually, we have to get back to this class here. This class is what I've called a ClientsideManager. This basically is managing all the connections for an incoming protocol in the financial industry called FIX, and what we're going to do is we are going to connect to the main matching engine by a pair of blocking queues. We break open the message, we send it down as an FXOrder, we get a response back. Basically, the communication to the matching engine is done with queues for a bunch of different reasons, not least of which helps with testing.

The thing I want to draw your attention to is this line here, because I've snuck in another new feature. This is also in Java 14 in preview mode. Do you see the syntax? if (response instaceof FXReject reject) This is combining two things at once. It's combining an instanceof test and a variable declaration. I could change the code here. I could write something like this. That is what we would expect. As Java programmers, I think we've been steered away over the years from doing much with instanceof. It's considered [inaudible 00:28:39]. Now, we can do this. It's clean, there's no unsightly cast, this looks like a small language feature. It's a very small language feature, but sometimes small language features are where this stuff starts from. This is called an instanceof pattern. For those of you who speaks smaller languages, maybe Scala, maybe Haskell, maybe some others, the word pattern has a different connotation here to how it's typically used in Java. I don't mean a software engineering pattern and I don't mean a regular expression. I mean the other type of pattern. This is the first example we see of it. Just another look to tease you there.

Equals Invariant

A couple of other things I want to tell you about records, and then we should move on and talk about sealed types. The first of them is that records have an additional equals invariant. This is in addition to your standard equals hashCode contract, but for records, you must also obey the following. If you take the individual components of the record and do a copy constructor based on it, basically, to say new record made up of the same components as the old one, then the copy must be equal to the original in a .equal sense. If you think about it, that follows directly from the semantics, the state, the whole state, and nothing but the state. If we mean that, this must be true.

Record Serialization

There's a second point which seems incredibly minor and trivial but is going to turn out to have a deep connection to the instanceof pattern we just meant. Serialization. There is a constraint on how records must be serialized. Serialization, as Brian [Goetz] tells us, is a second mechanism for constructor. It's invisible but public constructor and an invisible but public set of accessors for your internal state. The next line is "but not for records." Records must be serialized and deserialized using the idea that they are simply the composition of their state components. You must obey that rule, and if you do anything else, bad things are going to happen to you.

Java Switch Expressions

One other minor point that I want to make before we start to look at sealed types, and again, this will become obvious as to why, let's talk about enums. Specifically, let's talk about Java switch expressions. Hands up if you've seen a Java switch expression before. Lots of people have. It's quite nice. Personally, I would have much preferred it if we'd actually been able to call it something other than switch, but oh well, lost that one as well. The idea here is that now switch comes not only as a C-like statement form but also as something that must return expression. You can do some nice things in it. You can have multiple labels which correspond to the same thing. Notice that, in here, what we're using is an enum, DayOfWeek, after java.time. If it returns Saturday or Sunday, it's false, and if it returns Monday to Friday, it's true. Then, I'm going to handwave away doing bank holidays and anything like that out of this question.

There's something quite interesting about this. Can you see what's not present here that you might expect in other kinds of switches? The default case is missing. Why is the default case missing? Because I've covered the entire space. The enums are implementing the pattern. The pattern is called finitely many instances. This means that the compiler can check this code and know that it is impossible for it not to go down either branch. Quick teaser question, what happens if I, for some reason, I pass null into this?

Participant 1: No pointer exception.

Evans: No pointer exception, absolutely. This either throws an exception, if it doesn't throw an exception, all cases are covered, and the compiler can check that and verify that at compiled time.

Why Sealed Types?

Now, let's talk about sealed types. Let's remember one more time. Enums are instances of classes. Enums are exhaustive. Two questions arise. First of all, if I have a Pet as an enum and I have two instances, cat and dog, well, what happens if I want multiple cats or multiple dogs? How can I have a way of saying that a Pet object either is a cat object or is a dog object? That's a new type of OO construct. It's not straightforward "has a" or "is a." IS-A A or B. That's what the concept is. You may have seen this concept in other languages, in C#, in Scala, etc. It's actually quite an old idea. Probably little about what I'm talking about, actually, it's theoretically new, even if it's just mostly been surfaced in modern JVM languages like Scala and Kotlin.

Option 1 – The State Field

Returning to this, how do we do this? How do we represent this? How do we model this in existing Java syntax? Two options. Option one, the state field. You have a single class called Pet, which has a field of enum type that holds the "real" type. Effectively, we're breaking the OO missile for this. We are making the programmer keep track of the "type bits" by examining the field to say, "Is this a cat or is this a dog?" We're moving something which is in the proper domain of the type system down into the programmer's bookkeeping code, and that's horrible. The second problem that you have is that you can't have any type specifically or specific functionality. The cat can't purr. Your choice would be either cat doesn't purr or dog does purr. You either superset all the functionality into the base class or you disallow it altogether. If that starts to sound like a nasty ORM mismatch problem, that's because it essentially is the same thing. This is an ORM anti-pattern rewritten into pure Java code.

Option 2 – Abstract Base

Option two, the abstract base. We could start with an abstract base, a Pet class, with a package-private constructor and two separate concrete subclasses within the same package, and only they can call the package-private constructor so everything is fine, apart from this abstraction leaks. Outside of the JDK packages, there's no protection. Maybe modules help, but what do you do about if you need this construction in one of your API packages of the module? It can still be defeated there. What about reflection? That doesn't help either. Let's see how we actually do solve this.

Solution – New OO Construct

We actually introduce official supported sealed types. A sealed type is one that can be extended by a known list of types but no others. That's enforced, not a compiler level, but also a class file format and runtime level as well. There are different ways of thinking about them. In Java's case, the way that we think about them, we believe that we should treat them properly as almost final classes. They are a class which submits a known list of subtypes, but they're really part of the finality mechanism rather than anything else. Java as it sounds, we have two options, open and closed. You're final or you're open for extension by basically anyone. This is a halfway house, this is a middle ground between the two.

We've got two new keywords, sealed and permits. I teased those in a comment in the records code earlier. That's what they are. Notice that, just to build some bridges with other languages, these are also sometimes known as "union types" in other languages. Particularly, people talk about disjoint unions. Curiously, it's actually tied into a Java language feature that already exists. Sometimes big things grow from small places. Does anyone know the one place in the Java language where we already have something that looks a bit like this or something that looks like a union type?

Participant 2: [inaudible 00:38:06]

Evans: That would technically be an intersection type, I think, and we are going with that. It's related but it's not quite right.

Participant 3: Objects can be null.

Evans: Objects being null. No, the null type is special, so that's not really an answer either. That's some very interesting question, but that relates to things like how Kotlin handles nullability. I've run short of time so I'm going to call it here. Multicatch. The multicatch of expression, "is one of those or one of those or one of those." For those of you that speak other languages, notably Haskell, you can probably see why I'm shying away from calling these union types. It's to deal with the fact that the Java type system is single-rooted, at least for references.

There's something else we need to make all of this work. Hands up if you've heard of these things. It's not quite as well as I'd hoped. Java 11 Nestmates. Hands up if you've heard of Nestmates. Nobody, wow. Inner classes done correctly, fixing a design bug that goes all the way back to Java 1.1 actually making inner classes work the way that they always intend to do. With that, it's demo time.

Demo – Sealed Types

Unfortunately, sealed types aren't actually in Java 14 yet. Here's one I made earlier at my own personal build of OpenJDK in order to allow to play with sealed types. Here's what they look like, public abstract sealed class, permits Cat and Dog. This is a standard pattern to have the base as the abstract so that you don't actually have to deal with the supernode case. You are simply dealing with the possible disjoint subcases. We have a name, we have an abstract speaking method, and then we just have a very simple constructor. Now, we have a public final class. Yes, you do need to say final for all of these. I would much prefer if this was final by default, but there are some particularly bizarre use cases where you want not all of the classes which extend this to be final. Why you would ever want that backdoor to break the sealed-ness, I don't know, but there we go.

We have a constructor, and now we have a couple of things. We got the implementation of speak, which is part of the base functionality, but now, crucially, I've got my own functionality. I can go and hunt mice, which is handy if you're a cat. This is how it's laid out. Let's just show some compilation, javac. Something I realize, I didn't actually show you the bytecodes, the compilation record either, so let's just show that too.

Quite an interesting thing to notice here is that the Cat class really shows no real sign of having been the subtype of the sealed class at all, and it really doesn't need to. The actual sealing magic happens in the supertype. Now, you might notice something a bit weird down here with some invokedynamic, but don't be confused by that. That's simply the way that, in modern Java, things that are to do with shaping, such as string concatenation and various other methods, are now being built by invokedynamic factories. That basically is just the same as we do for lambdas, you're going to see that more and more. That dynamic stuff will be done using invokedynamic magic.

Let's actually look at this sealed type case. Again, nothing really to see here. We actually need to dig in. See, right down at the bottom, there's a new thing which says permitted subtypes. That means that this class, when you try to compile anything new, whether you have the original Java file for this or whether you have a class file compiled version of this, if you are not in this list, you will not get compiled. You will be rejected by the compiler. This solves the problems that we saw in the other half where with the package-private constructor, someone could still get around it. This also will prevent reflection as well. This really is a completely water type mechanism. Not so much to show in there.

Let's just, very quickly, switch to Java 14 again. It's going to look like this, Cashflow.class. Look, there's a new class called java.lang.Record. Just as for enums, if you try to directly extend that, the compiler won't let you. We have the public constructor, which has been automatically created and just basically does all of the things that you would expect a simple constructor to do. You have toString, hashCode, and equals, which are all provided for you by these invokedynamic factories that I was just talking about. Then you have some getter methods, and that's it. All of that boilerplate creation has been done in exactly the same way as it would be for enums.

The Path So Far

Here's the path so far. These are some of the pieces we needed to put together. Nestmates, inner classes done right. Switch expressions, which are now actually a standardized version of Java 14. Records, instanceof patterns. At some point, maybe 16, we'll get sealed types as well. There are also some other pieces which are coming into play behind the scenes. The instanceof pattern, a deconstruction pattern. We can now do things like think about how the instanceof pattern was written where you tested something and then you declared a variable. Imagine doing that to a record. What do we know about records? Records are just the product of their parts. What about declaring a new variable of record type but destructuring it in just the same way as we do in many other languages back into its components?

Conclusion

Those start to build towards a feature called pattern matching, not patterns as regular expressions, not patterns as language ideas, but the idea of being able to compose and decompose the structures of our objects on the fly. Hands up, have we got any Scala users? Think match expressions, with similar power, but implemented down in the runtime, down at VM level.

Records are about semantics. They implement a pattern. They're not a general replacement. They're very useful though. Sealed types are new OO construct, and together, they make up this idea called algebraic data types. For people that come from pure functional languages like Haskell, there are restrictions. The Java type system can't be fully modified to do exactly the same way that it works elsewhere, but this is our version of how algebraic data types will work. The big hope, and again, I work for a public company, nothing I can say should be taken as gospel, one that's forward-looking, hopefully, I would think that all of this would be final by Java 17. One thing I should also make clear is records have nothing to do with inline types. Inline classes and records are completely orthogonal and completely independent concepts. Records, sealed types, pattern matching by 17, yes, I think that's reasonable. I don't know about classes.

If you might want to grab a couple of URLs here, Brian [Goetz] was kind enough to do an article for us about records in InfoQ. A couple of my pieces about for Oracle's "Java Magazine," records and sealed types on JVM.

What can you do to help? Try out the new Java 14, and I suppose 15 betas now, because 14 will be released in a couple of weeks' time. Keep an eye out for the sealed types betas and deconstruction patterns. Please try and write some code using the new features. Even if it's just research or innovation time, as we call it at New Relic, give feedback. The sooner is better. I actually found that records were a fantastic feature to start using in code, and I hope to actually release that demo application I showed you as open source pretty soon.

Questions and Answers

Participant 4: Two questions. One, do records allow validation annotations on the parameters so that Java Beans validation, for example, can be triggered? Second, records are not Java Beans, but it is Java Beans now that is a lot of use this boilerplate and this is why we use Lombok. Will it really be so beneficial to the current applications and the current libraries that we use for all the Java Beans stuff?

Evans: Ok, so two good questions. The first of which is, yes, you can use validation annotations on them. There's some work to do to bridge this because you want to think carefully about what that means and make sure you don't bring in additional semantics. Secondly, it's unfortunate that they can't be easily retrofitted to Java Beans' capabilities, but the problem is you have other guarantees, like the copy constructor and the serialization, which you can't definitely rule in or out. People do some messed up things with Java Beans and people also use the Java Beans conventions when they also have additional semantics beyond what is meant in the Java Beans. Unfortunately, I think this is an example of a language feature where people are being cautious and just doing what you can with the first car. Will someone come up with a clever bridging thing? I'm sure they will. For all that Lombok has surface-level advantages, the more I've used it, the more I've come to realize that, actually, it's a bad solution. That's not to say anything bad about the technical ability of the people that wrote it. They did the best they could with the design space they had, but it doesn't make a good solution. The boilerplate, although it's bad, in my opinion, is the lesser of two evils compared to what Lombok does to you.

Participant 5: I also have two questions. First, with regards to sealed type, do the permitted types have to be in the same package as in your example or is it just for brevity?

Evans: I'm pretty sure it was just for brevity. It's a while since I wrote that code, but I think that as long as it's a correct fully qualified name, I think you're fine.

Participant 5: The second question, can records be generic?

Evans: No, they can't. There are good reasons for that. I'll tell you about it later if you want to know.

Participant 6: You gave a bit of a hint there, about permitted types not being final by default because there were good reasons for it. Could you then tell what those good reasons might be? That seems like quite a strong ant-pattern.

Evans: Not being final by default. I would have to point you to the appropriate place on the mailing list where Brian [Goetz] comes up with an example, where, in the example, it definitely would be problematic if they were final by default. This was part of the discussion about whether we should also have a keyword called non-final. Because the other proposal was to make them final by default. Introduce a new hyphenated keyword saying non-final and then basically allow that the other way around so that they were final by default but you could specify the non-final. I'd need to find the appropriate reference.

Participant 7: I'll follow that pattern of two questions. How do records differ from Scala case classes? Can you implement interfaces?

Evans: Yes, I showed an example of implementing interfaces. No, the Scala case classes are a good mental model, I think. They're similar in some ways. What will happen, in my opinion, given the way that we've seen this happen with other features in the Java language, is I wouldn't be at all surprised that in the future versions, Scala 3.3 or 3.5, that Scala case classes retrofitted on top of records. That's what happened with traits. Stateless traits just became interfaces with default methods. I think the same thing will happen here.

 

See more presentations with transcripts

 

Recorded at:

Mar 30, 2020

BT