Are you experienced with Java 11 and would you like to start writing applications in Java 17? Then you have to learn what new features are available. Of course, you can check new features of each consecutive Java version from 11 to 17. But why go around instead of directly checking a list of all new features available in Java 17 since 11? I have prepared one - the most important and useful language changes are in this article.
Java support
JDK 11 was officially released in 2018 and was supported until September 2023. Although extended support is still available until 2032, it is undoubtedly not worth building new applications on JDK 11. Actually, if your existing application still runs on JDK 11, it is the right time, or even already late, to upgrade to the new one.
Java 17 new features since 11
If you target JDK 17, you will appreciate this list of new features that were introduced in the language to make our - software developer's life easier.
Major Java 17 new features since 11:
- Text blocks
- Switch Expressions
- Records
- Sealed classes
- Pattern matching for instanceof
- Helpful NullPointerException
- Compact number formatting
- Day Period Support
Text blocks
In Java 11, when you wanted to create a static multiline text, you had to explicitly put new line characters in it. Additionally, if you wanted to keep it visually multiline in the code, there was no better way than using multiple strings - one for each line and concatenating them with a plus sign.
String j11MultilineText = "line1\n" +
"line2\n" +
"line3";
System.out.println(j11MultilineText);
That was especially annoying with JSON texts, which already became very popular. To get a nicely formatted JSON value like this:
{
"field1": "value1",
"field2": 123,
"field3": {
"innerField": "value3"
}
}
you had to put this monster to the code:
String j11Json = "{\n" +
"\t\"field1\": \"value1\",\n" +
"\t\"field2\": 123,\n" +
"\t\"field3\": {\n" +
"\t\t\"innerField\": \"value3\"\n" +
"\t}\n" +
"}";
System.out.println(j11Json);
There is no doubt, that it was inconvenient. Fortunately, JDK 15 introduced JEP 378 Text Blocks. Since then, it is much easier. You can simply write the same code like this:
String j17MultilineText = """
line1
line2
line3""";
System.out.println(j17MultilineText);
String j17Json = """
{
"field1": "value1",
"field2": 123,
"field3": {
"innerField": "value3"
}
}""";
System.out.println(j17Json);
The result is exactly the same - nicely formatted output. The real difference is in the readability. It is worth mentioning that escaping quotation marks is no longer needed. The code is easier to write, modify, and read.
Switch expressions
Oracle fought with switch expressions for two consecutive releases: 12 and 13 to make it final in JDK 14 as JEP 361. We got not one, not two, but three important improvements to the switch expressions.
In Java 11, a properly used switch expression looked like this one:
String option = "a";
switch (option) {
case "a":
System.out.println("found a");
break;
case "b":
System.out.println("found b");
break;
default:
System.out.println("unknown found");
}
It checks a value in the option
variable and goes through cases to decide, which one matches. The first improvement is eliminating the need to use the break
keyword. As you can still use the old syntax, the new one is indicated by using an arrow. So this switch can be rewritten to a simpler version permanently available since JDK 14:
String option = "a";
switch (option) {
case "a" -> System.out.println("found a");
case "b" -> System.out.println("found b");
default -> System.out.println("unknown found");
}
That was only a cosmetic change making the code more readable. The second improvement allows returning a value directly from the switch statement and assigning it to a variable.
String option = "a";
String result = switch (option) {
case "a" -> "found a";
case "b" -> "found b";
default -> "unknown found";
};
System.out.println(result);
We could only dream about such a convenient assignment in Java 11. Now, the arrow operator automatically means returning a value, which can be further assigned. The third improvement is a small enhancement to the second one. It often happens that before returning a variable you want to do some action in the case section. Then, Java isn't sure which value you want to return. So the yield
keyword was introduced.
String option = "a";
String result = switch (option) {
case "a" -> {
System.out.println("some action");
yield "found a";
}
case "b" -> "found b";
default -> "unknown found";
};
System.out.println(result);
The yield keyword points out a value that is supposed to be returned in a particular case.
Records
An official shape of records was finally released with JDK 16 as JEP 395, but Java developers could play with an initial version of records starting with JDK 14.
Developers have complained about Java for a long time. One of the main targets was the amount of boilerplate code. That was often referred to as too much ceremony. The world does not like emptiness and tries to fulfill it. The software development part of our world is not an exception. Actually, it is a great example. If enough developers need a tool or library, someone always shows up shortly after discovering the need and builds such a library. A well-known answer to the boilerplate code complaint in Java is Lombok.
Now, finally, after several years Java also offers a partial relief - records.
A record is a final immutable class, that already has autogenerated constructor, getters, equal, hash, and toString methods. You should be able to use records in most cases where would normally implement immutable classes. Natural examples are DTO objects.
public record City(int id, String name) {
}
The above code snippet is a definition of a City
record. It has two fields: id
and name
. This amount of code is enough to tell JVM to generate a constructor with both fields as arguments, getters, equal, hash, and toString methods. After this, you can safely create a City object and call getters.
City cracow = new City(1, "Cracow");
System.out.println(cracow.id() + " - " + cracow.name());
I think, even at this point, records would be useful. But Oracle didn't stop and added a possibility to define non-canonical constructors and other methods.
public record City(int id, String name) {
public City(String name) {
// non-canonical constructor must delegate to another constructor
this(0, name);
}
// can define other methods
public String getHello() {
return "It is " + name + " city with id=" + id + ".";
}
}
They can be used like in the below code.
City cracow = new City(1, "Cracow");
System.out.println(cracow.id() + " - " + cracow.name());
System.out.println(cracow.getHello());
City warsaw = new City("Warsaw");
System.out.println(warsaw.getHello());
Sealed classes
Those who often create final classes may appreciate sealed classes delivered as JEP 409 with JDK 17. The class access system in Java is rather simple. More advanced accessibility controls were proposed in the Jigsaw project with Java 9. Sealed classes are another step towards better control over who can extend what class. Here is an example.
The Publication class is abstract. Two classes extend it: Book and Magazine. The problem begins when I don't want to allow more classes to extend Publication, but I still agree to use this abstraction externally. Sealed Publication class helps to address that by explicitly defining which classes are allowed to extend it.
public abstract sealed class Publication permits Magazine, Book {
}
Then, Magazine and Book can freely extend Publication.
public non-sealed class Book extends Publication {
}
public final class Magazine extends Publication {
}
Extending a sealed class comes with a mandatory defining each child class as non-sealed or final. Final traditionally prohibits further extensions. Non-sealed class can still be extended.
Pattern matching for instanceof
So called pattern matching for instanceof is a handy language enhancement introduced with JEP 394 in Java 16. The instance of statement in Java is problematic in so many ways, but at least one problem was addressed in Java 16. The instance of statement is mainly used as a condition in an if statement to check if an object is an instance of a specific class. If so, the object is usually cast to that class to allow using methods of this class. The pattern is so common, that Java developers decided to make our life easier and shorten it a little bit. Now, checking the object's class and the casting is done by a single construct.
Publication pub = new Book();
if (pub instanceof Book book) {
System.out.println(book);
}
If the pub
variable is an instance of the Book
class, it is immediately cast to Book
and assigned to a new book
variable.
Helpful NullPointerException
NullPointerException has been a pain for so many years. You want to write a compact piece of code, so it is easy to read. Then it hits your back with a NullPointerException. Yes, it tells you what line number it occurred with the whole stacktrace, but is not enough. That line contains so many method invocations and cascade usage of several variables and fields. So which one is causing the problem? There was no easy answer before JEP 358 in Java 14.
Gladly, NullPointerExcecption is more elaborate now.
City city = new City(99, null);
System.out.println(city.name().toUpperCase());
Obviously, the second line will throw the exception, no big change there. But look at the exception message:
java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because the return value of "com.dbapresents.jdk17newfeatures.City.name()" is null
It tells us exactly which field was null!
Compact Number Formatting Support
Java 12 was released with a new way of formatting numbers. Except for choosing a language through a locale, you can also pick SHORT
or LONG
version.
NumberFormat fmtShort = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);
System.out.println(fmtShort.format(90000));
NumberFormat fmtLong = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);
System.out.println(fmtLong.format(90000));
The output is this:
90K
90 thousand
Day Period Support
Being on the topic of formatting, you can also take advantage of adding a day period name to the formatted time. It is known as Day Period Support and is included in Java 16. It is encapsulated in the B
pattern.
String time = DateTimeFormatter.ofPattern("B").format(LocalTime.of(14, 0));
System.out.println(time);
The output is:
in the afternoon
Summary
These are the main new features that can be used in Java 17, that were not available in Java 11. I think it is worth knowing that list when starting to use a new JDK, because they open new possibilities to the programmers. Of course, if you use an advanced IDE, it may suggest new language features, but still. Getting familiar with those features is not time-consuming and I hope that article was useful to you.