In September 2021, Oracle released Java 17, the next long-term support release of Java. If you’re using Java 8 or Java 11, you probably haven’t noticed some cool new features that have been added since Java 12.
Since this is an important release, I’m going to highlight some new features that I’m personally interested in!
It is important to note that most changes in Java first go through the “preview” phase, meaning they are added to a release but are not yet complete. People can try them out, but it’s not recommended to use them in a production environment.
All of the features listed here have been officially added to Java and have gone through the preview phase.
1: sealing class
The seal class, which was in the preview phase in Java 15 and became an official feature in Java 17, provides a new way to qualify inheritance rules. When you add the sealed keyword before a class or interface, you also add a list of classes that allow you to extend the class or implement the interface. For example, if you define a class:
public abstract sealed class Color permits Red, Blue, Yellow
Copy the code
That is, only Red, Blue, and Yellow can inherit from this class, and no other class can compile to inherit from it.
You can also omit permits and place the class definition in the same file as the class, as shown below:
public abstract sealed class Color {... }... class Red extends Color {... }... class Blue extends Color {... }... class Yellow extends Color {... }Copy the code
Note that these subclasses are not nested within the enclosing class, but are placed after the class definition. This has the same effect as using the permit keyword; the only classes that can extend Color are Red, Blue, and Yellow.
So, where are the seal classes usually used? By limiting inheritance rules, you also limit encapsulation rules. Suppose you are developing a library and need to include the abstract class Color. You know the Color class and which classes need to extend it, but if it’s declared public, is there anything you can do to prevent external code from extending it?
What if someone misunderstands its purpose and extends it with Square? Does that suit your intentions? Or do you actually want to keep Color private? But even then, package-level visibility is not immune to all problems. What if someone later extends the library? How will they know that you only intend to integrate Color with a small number of classes?
Sealing classes are not only a way to protect your code from external code, but also a way to communicate intent to people you may never have met. If a class is sealed, you are communicating that only certain classes can extend it. This robustness ensures that anyone who reads your code years from now will understand its rigor.
2: enhanced null pointer exception
The enhanced null-pointer exception is an interesting update — not too complicated, but still welcome. This enhancement, officially released in Java 14, improves the readability of nullpointerExceptions (NPE) by printing out the name of the method called at the point where the exception was thrown and the name of the null variable. For example, if you call a.b.GoetName () and b is empty, the stack trace of the exception will tell you that the call to getName() failed because B is empty.
As we all know, NPE is a very common exception, and while it’s not hard to figure out the root cause of throwing exceptions in most cases, you’ll occasionally run into two or three suspicious variables at the same time. You go into debug mode and start looking at the code, but the problem is hard to reproduce. You can only try to remember what you did to cause the NPE to be sold in the first place.
If you can get this information ahead of time, you can save yourself all the trouble of debugging. That’s the beauty of this feature: no more guessing where the NPE came from. When the chips are down, when you encounter an unusual scenario that is hard to reproduce, you have everything you need to solve the problem.
This is an absolute lifesaver!
3: switch expression
I hope you’ll bear with me — the Switch expression (preseen in Java 12 and officially added to Java 14) is a combination of switch statements and lambda. Really, when I first described switch expressions to people, I said they lambdas the switch statement. Take a look at this syntax:
String adjacentColor = switch (color) { case Blue, Green -> "yellow"; case Red, Purple -> "blue"; case Yellow, Orange -> "red"; default -> "Unknown Color"; };Copy the code
Do you see what I mean now?
One obvious difference is that there is no break statement. Switch expressions continue Oracle’s trend of making Java syntax more concise. Oracle hates that most switch statements contain a lot of CASE BREAK, CASE BREAK, CASE BREAK… .
To be honest, they are right to hate this, because it is easy to make mistakes in this place. Can any of us say they’ve never forgotten to add a break statement to the switch, only to know when the code crashes at run time? The switch expression fixes this problem in an interesting way, by separating all values in the same code block with commas. That’s right, no need to use break! It will take care of it for you!
The switch expression also adds the yield keyword. If a case enters a code block, yield will be used as the return statement of the switch expression. For example, if we modify the above code slightly:
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"; }};
Copy the code
In the default case, the system.out.println () method will be executed and the final value of the adjacentColor variable will be “Unknown Color” because that’s what yield returns.
In general, the switch expression is a more concise switch statement, but it does not replace the switch statement, and both statements are available.
4: text block
The text block feature, previewed in Java 13 and officially added to Java 15, simplifies writing multiline strings, supports line breaks, and preserves indentation without the need for escaping characters. To create a text block, just do this:
String text = """HelloWorld""";
Copy the code
Note that this variable is still a string, but it implies newlines and tabs. Also, if we want to use quotes, we don’t need escape characters:
String text = """You can "quote" without complaints!" ""; // You can "quote" without complaints!Copy the code
The only place you need to use a backslash to escape a character is when you want to include “”” in a text block:
String text = """The only necessary escape is \""",everything else is maintained.""";
Copy the code
In addition, you can call the format() method of String to replace placeholders in a text block with dynamic content:
String name = "Chris"; String text = """My name is %s.""".format(name); // My name is Chris.Copy the code
The space at the end of each line is clipped out unless you specify ‘\s’, which is an escape character for the text block:
String text1 = """No trailing spaces.Trailing spaces. \s""";
Copy the code
So, when are text blocks used? In addition to being able to format large chunks of text, pasting snippets of code into strings is very easy. Because the indentation is preserved, if you’re writing a block of HTML or Python code, or any other language, you can write them in the normal way and then enclose them in brackets to preserve the format of the code. You can even write JSON in text blocks and easily insert values using the format() method.
Overall, this is a very convenient feature. While text blocks may seem like a small feature, in the long run, small features like these will increase development efficiency.
5: record class
The Record class, previewed in Java 14 and officially added to Java 16, is a data class that handles all of the boilerplate code associated with POJOs. That is, if you declare a record class:
public record Coord(int x, int y) {}
Copy the code
The equals() and HashCode () methods are implemented automatically, toString() returns the values of all fields contained in the class instance, and most importantly, x() and y() return the values of x and y, respectively. Think about the POJO classes you’ve written before, and imagine replacing them with the Record class. Isn’t it better? How much work has been saved?
In addition, the Record class is final and immutable — it cannot be inherited, and once a class instance is created, its fields cannot be modified. You can declare methods in the Record class, including non-static and static methods:
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; }}record class can have multiple constructors: public record Coord(int x, int y) {public Coord() {this(0,0); // The default constructor is still implemented. }}Copy the code
Note that when you declare a custom constructor in the Record class, you must call the default constructor. Otherwise, the Record class will not know what to do with its value. If you declare a constructor that is the same as the default constructor, you initialize all 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.}
Copy the code
There is much to discuss about the Record class. This is a big change, and when used in the right place, they can be very useful. I haven’t covered everything here, but hopefully this gives you an idea of the capabilities they offer.
6: Mode matching
Pattern matching is another move Oracle has made in its battle with Java’s verbose syntax. Pattern matching, previewed in Java 14 and Java 15 and officially added to Java 16, eliminates unnecessary type conversions once the Instanceof condition is satisfied. For example, we’re all familiar with code like this:
if (o instanceof Car) { System.out.println(((Car) o).getModel()); }Copy the code
This is necessary if you want to access Car’s methods. In the second line, o is an instanceof Car, which is unquestionable, as instanceof has confirmed. If we use pattern matching, just make one small change:
if (o instanceof Car c) { System.out.println(c.getModel()); }Copy the code
All object type conversions are now done by the compiler. It looks like a small change, but it avoids a lot of boilerplate code. This also applies to conditional branches, when you enter a branch where the object type is already specified:
if (! (o instance of Car c)) { System.out.println("This isn't a car at all!" ); } else { System.out.println(c.getModel()); }Copy the code
You can even use pattern matching in the Instanceof line:
public boolean isHonda(Object o) { return o instanceof Car c && c.getModel().equals("Honda"); }Copy the code
Pattern matching is not as big as some of the other changes, but it simplifies common code.
Java 17 continues to evolve
Of course, these aren’t the only updates Java 12 to Java 17 is rolling out; they’re just the ones THAT I find interesting. It takes a lot of courage to run a large project with the latest version of Java, especially if you’re migrating from Java 8.
If someone is hesitant, it’s understandable. But even if you don’t have a migration plan, or an upgrade plan may take years, it’s always good to keep up with new language features. I hope that the content I’ve shared has helped them sink deeper into the hearts of people who read them and start thinking about how to use them!
Java 17 is special — it’s the next long-supported release, picks up the baton from Java 11, and is likely to be the most migrated Java release in the next few years. Even if you’re not ready to start learning yet, you’re already an experienced developer working on a Java 17-based project!
PS: In case you can’t find this article, please click “like” to browse and find it