Key Takeaways
- Java 17, the newest long-term support release of Java, was made available to the Java community in September 2021.
- All the features reviewed here have officially been added to the language, having completed their "preview" phases.
- These features are purely syntactic, all being new types of classes and keywords. There are many more changes worth researching!
- Although very cutting-edge, Java 17 is a complete and stable version of Java, and is definitely worth learning or even migrating your applications to this version.
In September 2021, Oracle rolled out Java 17, the next long-term support release of Java. If you’ve been primarily working in Java 8 or Java 11, you might not be aware of some of the cool additions from Java 12 onward.
Seeing as this is a significant release, I thought it would be nice to highlight some of the new features that have personally piqued my interest!
Note, most changes in Java are first “previewed,” which means they are added to a release, but they aren’t considered finished yet. People are encouraged to experiment with them, but discouraged from using them in production code.
All the features I’ve singled out have been officially added to Java, and are past their preview phases.
1: Sealed Classes
Sealed Classes, previewed in Java 15 and officially added in Java 17, are a new means of enforcing rules on inheritance. When you add the sealed keyword to the definition of a class or interface, you also add a list of classes that are permitted to extend or implement it. For instance, if you create a class defined as:
public abstract sealed class Color permits Red, Blue, Yellow
Then only the classes Red, Blue
and Yellow
may extend it. Anything outside the pre-defined batch will fail to compile.
You can also forgo the permits
keyword entirely, and have all the sealed class definitions kept in the same file as the class itself, as follows:
public abstract sealed class Color {...}
... class Red extends Color {...}
... class Blue extends Color {...}
... class Yellow extends Color {...}
Notice, these subclasses aren't nested in the sealed class, but rather come after the class definition. This is functionally the same as using the permits
keyword; Red, Blue
and Yellow
remain the only classes that can extend Color
.
So where are sealed classes useful? Well, by enforcing rules on inheritance, you're also enforcing rules on encapsulation. Consider the case where you're building a software library, and you need to include the abstract class
Color
. You know what Color
is, and what needs to extend it, but if it’s declared as public, what's stopping outside code from extending it?
What if someone misunderstands its purpose and extends it to Square? Was that your intent? Or did you want to keep Color internal? Even then, package-level visibility doesn't prevent all issues. What if someone expands on the library later down the line? How will they know you only intended Color for a small subset of classes?
Sealed classes not only protect your code from outside sources, but they communicate intent to people you might never meet. If a class is sealed, you are saying these are the classes meant to extend it, and no others. It's an interesting sort of robustness that ensures that, years down the line, anyone reading your code will understand the strictness of what you wrote.
2: Helpful Null Pointers
Helpful Null Pointers are an interesting update – not overly complicated, but still a welcome change to the language. Officially delivered in Java 14, Helpful Null Pointers improve the readability of null pointer exceptions (NPEs), printing out the name of the call that threw the exception, as well as the name of the null variable. For instance, if you called a.b.getName()
, and b was undefined, the stack trace for the exception would say that getName()
failed because b is null.
Now, we've all been there before. NPEs are a very common exception, and while most of the time it's not hard to pick out the culprit, every now and then, you get a case where two or three variables could be responsible. So you hop into debug, start crawling through the code, and maybe you have trouble reproducing the issue. So you try to remember what you did to create the NPE in the first place.
If only you could’ve been given the information upfront, you wouldn't have to go through all this debugging hassle. That's where Helpful Null Pointers shine: no more guessing NPE's. In a clutch moment, when you've got a scenario difficult to recreate, you have everything you need to solve the issue as soon as the exception happens.
This could be an absolute lifesaver!
3: Switch Expressions
I need you to bear with me for a second – Switch Expressions (previewed in Java 12, and added officially in Java 14) are a somewhat marriage between switch statements and lambdas. Really, the first time I ever described Switch Expressions to someone, I said they lambdafied switches. For some context, take a look at the syntax:
String adjacentColor = switch (color) {
case Blue, Green -> "yellow";
case Red, Purple -> "blue";
case Yellow, Orange -> "red";
default -> "Unknown Color";
};
See what I mean?
One glaring difference is the absence of breaks. Switch Expressions continue a trend Oracle has of reducing the verbosity of Java, which is to say Oracle really hated how most switch statements went CASE BREAK CASE BREAK CASE BREAK etc.
And honestly, they were right to hate that, because it was highly susceptible to human error. Can any among us say they haven't forgotten a break statement on a switch, only to be alerted when our code freaked out at runtime? Switch Expressions fix this in a fun way, by letting you just comma separate all the values that fall into the same block. That's right, no more breaks! It handles it on its own now!
Another important part of Switch Expressions is the new yield
keyword. Should one of your cases feed into a block of code, yield
is used as the return statement of the Switch Expression. For example, if we slightly modify the code block from above:
String adjacentColor = switch (color) {
case Blue, Green -> "yellow";
case Red, Purple -> "blue";
case Yellow, Orange -> "red";
default -> {
System.out.println("The color could not be found.");
yield "Unknown Color";
}
};
In the default case, the System.out.println(
) method will be executed, and the adjacentColo
r variable would still end up being "Unknown Color," because that's what the yield expression returns.
Overall, Switch Expressions are much cleaner, much more condensed switch statements. That being said, they don't replace switch statements, and both are still available for use.
4: Text Blocks
First previewed in Java 13, and added officially in Java 15, Text Blocks are a means to simplify writing multi-line strings, able to interpret new lines and maintain indentation without the need for escape characters. To create a Text Block, you just need to use this syntax:
String text = """
Hello
World""";
Note, this value is still a String, but new lines and tabs are implied. Likewise, if we want to use quotes, there's no need for any escape characters:
String text = """
You can "quote" without complaints!"""; // You can "quote" without complaints!
The only time you'd need a backslash escape would be if you were trying to literally write """:
String text = """
The only necessary escape is \""",
everything else is maintained.""";
Besides that, you can call String’s format() method directly on what you’ve written, which allows us to easily replace information inside Text Blocks with dynamic values:
String name = "Chris";
String text = """
My name is %s.""".format(name); // My name is Chris.
Trailing spaces are also clipped per line, unless you specify with '\s', a text block-specific escape character:
String text1 = """
No trailing spaces.
Trailing spaces. \s""";
So in what situations would we use Text Blocks? Well, besides being visually able to bake in the formatting for a large block of words, Text Blocks make it infinitely easier to paste snippets of code into strings. Since indentation is preserved, if you were to write a block of HTML or Python, or any language really, you'd be able to write it normally and just wrap it in """. That's all you need to preserve the formatting. You can even use Text Blocks to write JSON, and use the format()
method to easily plug in values.
All in all, a very convenient change. Although Text Blocks seem like a small feature, these quality-of-life features add up in the long run.
5: Record Classes
Records, previewed in Java 14 and officially added in Java 16, are data-only classes that handle all the boilerplate code associated with POJO's. That is to say, if you declare a Record as follows:
public record Coord(int x, int y) {
}
Then equals()
and hashcode()
methods are implemented, toString()
will return a clean printout of all the Record's values, and most importantly, x()
and y()
will be methods that return, respectively, the values of x
and y
. Think about the ugliest POJO you've ever seen, and imagine implementing it with Records. How much nicer would that look? How much more cathartic would that be?
Besides that, Records are both final and immutable – no extending records, and once a Record object is created, its fields cannot be changed. You are allowed to declare methods in a Record, both non-static and static:
public record Coord(int x, int y) {
public boolean isCenter() {
return x() == 0 && y() == 0;
}
public static boolean isCenter(Coord coord) {
return coord.x() == 0 && coord.y() == 0;
}
}
And Records can have multiple constructors:
public record Coord(int x, int y) {
public Coord() {
this(0,0); // The default constructor is still implemented.
}
}
Note, when you declare a custom constructor inside the Record, it must call the default constructor. Otherwise, the record wouldn't know what to do with its values. If you declare a constructor that matches the default, that's fine, so long as you also initialize all of the Record's fields:
public record Coord(int x, int y) {
public Coord(int x, int y) {
this.x = x;
this.y = y;
} // Will replace the default constructor.
}
There's a lot to talk about regarding Records. They're a big change, and they can be incredibly useful in the right situations. I haven't covered everything here, but hopefully this gives you the gist of what they’re capable of doing.
6: Pattern Matching
Another development in Oracle's war on verbosity is Pattern Matching. Previewed in Java 14 and Java 15, and officially released in Java 16, Pattern Matching is a means to get rid of needless casting after an instanceof
condition is met. For example, we're all familiar with this situation:
if (o instanceof Car) {
System.out.println(((Car) o).getModel());
}
Which, of course, is necessary if you want to access the Car
methods of o. That being said, it is also true that on the second line, there is no question that o is a Car
– the instanceof
has already confirmed that. So with Pattern Matching, a small change is made:
if (o instanceof Car c) {
System.out.println(c.getModel());
}
Now all the nitty-gritty of casting the object is done by the compiler. Seemingly minor, but it cleans up a lot of boilerplate code. This also works on conditional branches, when you enter a branch where it's apparent what type the object is:
if (!(o instance of Car c)) {
System.out.println("This isn't a car at all!");
} else {
System.out.println(c.getModel());
}
You can even use Pattern Matching on the same line as the instanceof itself:
public boolean isHonda(Object o) {
return o instanceof Car c && c.getModel().equals("Honda");
}
Although not as grandiose as some of the other changes, Pattern Matching is an effective cleanup to a common source of code noise.
Java Continues to Grow in Java 17
These aren’t the only updates from Java 12 to Java 17, of course, but these are a few I found interesting. It takes a lot of guts to run your big project on the most cutting-edge version of Java, more so if you’re migrating from Java 8.
If people are hesitant, that makes sense. However, even if you don’t have plans to move, or that particular upgrade is slated for years down the line, it’s always good to keep up with the new features being built into the language. My hope is that I’ve presented these concepts in a way that makes them stick, so that anyone reading might even start thinking about ways to use them!
Java 17 is special – it’s the next long-term support release, taking the mantle from Java 11, and will likely be the most-migrated-to version of Java within the coming years. Even if you’re not ready now, it’s never too early to start learning, and when you finally find yourself on a Java 17 project, you’ll already be a cutting-edge developer!
About the Author
Christopher Bielak is a Java Developer with Saggezza (an Infostretch company), who has a keen interest in the development and teaching of software languages. He has a decade’s worth of experience working in banks and finance, and often beats the drum for moving towards more cutting-edge technologies.