Java 17 – What’s new?

Java 17 was released on 21 September 2021 as the next Long Term Support (LTS) release. This means that its premier support is going to last for another 5 years, and its extended support – 8 years.

A list of LTS releases (previous and upcoming) is shown below:

  • Java 7 – released in July 2011
  • Java 8 – released in March 2014
  • Java 11 – released in September 2018
  • Java 17 – released in September 2021
  • Java 21 – scheduled for September 2023

After Java 17, the time in between releases is planned to be shortened to 2 years (to be confirmed by ORACLE). In this article, I present the changes that have been made after the previous version of LTS – Java 11.

Java 11 to 17 changes

Changes from the last LTS release included over 40 JDK Enhancement Proposals (JEP). This article is not going to cover every single one of them. Instead, we will focus on the biggest features added within all smaller Java releases from Java 11 to 17. Changes will be presented in such versions, in which they were officially released, not in the ones that appeared for the first time as a preview feature. 

For detailed information about all JEPs included in the versions listed below, please visit: JEPs in JDK 17 integrated since JDK 11.

Java 12

Compact NumberFormat – is a new formatter class which converts numbers to a more compact format.

For example:


NumberFormat fmtShort = NumberFormat
       .getCompactNumberInstance(Locale.UK, NumberFormat.Style.SHORT);

System.out.println(fmtShort.format(1000));
System.out.println(fmtShort.format(1000000));

NumberFormat fmt = NumberFormat
       .getCompactNumberInstance(Locale.UK, NumberFormat.Style.LONG);

System.out.println(fmt.format(1000));
System.out.println(fmt.format(1000000));

Output:


1K
1M
1 thousand
1 million

Teeing Collector is a new collector in which every element is processed by two downstream collectors, and returned as a result of a specified merge function.

In this example, teeing collector is going to be used to calculate the average of the students’ grades.


List<Integer> grades = List.of(3, 5, 5, 2, 5, 3, 4, 3, 5, 4, 4, 2);

var averageGrade = grades.stream()
       .collect(
               teeing(
                       summingDouble(i -> i),
                       counting(),
                       (sum, n) -> sum / n
               ));

System.out.println(averageGrade);

Output:


3.75

Java 13

No significant features were officially released in this particular version. However, later in this article, I will discuss a feature officially implemented in Java 15, which was first introduced as a preview in Java 13.

Java 14

Switch Expressions – first introduced in Java 12, now officially released; a fine addition to regular switch statements. Let’s cover the implemented differences in the examples below.

Example:

Earlier, Java 14 regular switch statement looked like this:


String messageOfTheDay = "";

DayOfWeek dayOfTheWeek = DayOfWeek.MONDAY;

switch (dayOfTheWeek) {
   case MONDAY:
       messageOfTheDay = "Two days till Wednesday!";
       break;
   case TUESDAY:
       messageOfTheDay = "One day till Wednesday!";
       break;
   case WEDNESDAY:
       messageOfTheDay = "It is Wednesday my dudes!";
       break;
   default:
       messageOfTheDay = "";
}

What if someone wanted to have a value immediately assigned to a variable without having to remember about predefining it earlier? The answer to that is simple: switch expressions.

Example:


DayOfWeek dayOfTheWeek = DayOfWeek.MONDAY;

String messageOfTheDay = switch (dayOfTheWeek) {
   case MONDAY -> "Two days till Wednesday!";
   case TUESDAY -> "One day till Wednesday!";
   case WEDNESDAY -> "It is Wednesday my dudes!";
   default -> "";
}

For more complicated operations than simply assigning a value to a result of a cause clause, a switch expression provides a keyword yield that causes one to create a specified value along with other actions in a given code block.


DayOfWeek dayOfTheWeek = DayOfWeek.MONDAY;

String messageOfTheDay = switch (dayOfTheWeek) {
   case MONDAY -> {
       System.out.println(dayOfTheWeek);
       yield "Two days till Wednesday!";
   }
   case TUESDAY -> {
       System.out.println(dayOfTheWeek);
       yield "One day till Wednesday!";
   }
   case WEDNESDAY -> "It is Wednesday my dudes!";
   default -> "";
};

System.out.println(messageOfTheDay);

Note that yield keyword may only be used in switch expressions, not in switch statements.

Helpful NullPointerExceptions – Sometimes, in complicated structures it is very difficult to define which object was null, but rest assured – Java 14 comes to your rescue with a more detailed description of the NullPointerException.

Example:


String horsePower = car.getEngineDetails().getHorsePower().toUpperCase();

System.out.println(horsePower);

A traditional NullPointerException program will simply return

Output:


Exception in thread "main" java.lang.NullPointerException
	at com.company.Main.main(Main.java:44)

However, with Helpful NullPointerException enabled (by default from Java 15) we get

Output:


Exception in thread "main" 
java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because the return value of "com.company.Engine.getHorsePower()" is null
	at com.company.Main.main(Main.java:44)

As you can see, we get the information about specific operations that cannot be performed, and exact locations of null values.

Java 15

Text blocks – feature first introduced in Java 13, now finally released! 

So, what is a text block? In simple words: it is a multiline String.

Example, before text block:


String detailsJsonString = "{\n" +
       "    \"name\": \"John\",\n" +
       "    \"lastName\": \"Doe\",\n" +
       "    \"experience\": \"3 years\",\n" +
       "    \"description\": \"This is description\"\n" +
       "}";

After adding text block:


String detailsJsonTextBlock = """
       {
           "name": "John",
           "lastName": "Doe",
           "experience": "3 years",
           "description": "This is description"
       }
                             """;

Additionally, a few String methods came along with text blocks:

  • stripIndent() – removes unnecessary blank spaces
  • translateEscapes() – returns a String with escape characters
  • formatted() – formats a String using the same String as a template

Java 16

Record Class – the biggest feature released in Java 16 is record class.

So, what does it do? It is an immutable Java Class that greatly reduces the amount of boilerplate code.

Example:

Immutable data class before record:


public final class Bicycle {

   private final String color;
   private final String wheelSize;


   public Bicycle(String color, String wheelSize) {
       this.color = color;
       this.wheelSize = wheelSize;
   }

   public String getColor() {
       return color;
   }

   public String getWheelSize() {
       return wheelSize;
   }

   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (!(o instanceof Bicycle)) return false;

       Bicycle bicycle = (Bicycle) o;

       return Objects.equals(color, bicycle.color)
               && Objects.equals(wheelSize, bicycle.wheelSize);
   }
  
   @Override
   public int hashCode() {
       return Objects.hash(color, wheelSize);
   }

   @Override
   public String toString() {
       return "Bicycle{" +
               "color='" + color + '\'' +
               ", wheelSize='" + wheelSize + '\'' +
               '}';
   }

As you can see, even in a simple two-field Class, there is quite a lot of boilerplate code. Now, let’s get to the same result using Record Class:


public record Bicycle(String color,
                      String wheelSize) {

}

Record Class by default generates all the methods above, and greatly speeds up the process of writing immutable Java Classes.

Pattern Matching for instanceof – a helpful new feature, first introduced in Java 14. It shortens type checking.

Example:

First, let’s see the piece of code in the form prior to implementing Pattern Matching for instanceof:


public boolean equals(Object o) {
   if (this == o)
       return true;
   if (!(o instanceof Bicycle))
       return false;

   Bicycle bicycle = (Bicycle) o;

   return Objects.equals(color, bicycle.color)
           && Objects.equals(wheelSize, bicycle.wheelSize);
}

Now, with Pattern Matching implemented we get:


public boolean equals(Object o) {
   if (this == o)
       return true;
   if (!(o instanceof Bicycle bicycle))
       return false;

   return Objects.equals(color, bicycle.color)
           && Objects.equals(wheelSize, bicycle.wheelSize);
}

Now, there is no need to declare a variable for any object that was already checked. 

.toList() stream method – one last thing worth mentioning that was released in Java 16. It is the introduction of the .toList() method used in Java streams.

.toList() should not be confused with shorthand to .collect(Collectors.toList()). Below I will present key differnces:

.collect(Collectors.toList()):

  • is mutable* can be sorted or added to
  • return type java.util.ArrayList

* – the current method of Collectors.toList() creates mutable List but states that it’s not guaranteed on the type, mutability, serializability, or thread-safety of the List.

.toList()

  • is immutable cannot be sorted or added to
  • return type java.util.ImmutableCollections.ListN

In simple words, we should use toList() when we know the size of the Stream and it will not be further modified.

Java 17

Sealed Class – another modifier, introduced in Java 15, released in Java 17. 

It is used to determine which Classes are permitted to inherit from the parent Class. For creators of JDKs and Frameworks, this feature may prove very useful. It gives them more control over which Classes are permitted to extend chosen types created by them.

Example


public sealed class Animal permits Dog, Cat {
}

Sealed Class requires its subclasses already defined using keyword permits. 

Additionally, subclasses must use one of the following modifiers:

  • final – closed for extensions
  • sealed – extension only for permitted subclasses
  • non-sealed – open for extensions

Conclusion

To sum up, many Java versions have been released since the previous LTS version, but only a few of them have introduced some truly significant features. 

Record Classes feature seems to be the biggest of them. As we have stated before, it has a huge impact on removing the major amount of the boilerplate code. Another extremely useful thing might be the introduction of Switch Expression, which is a fine modern alternative to the regular Switch Statements.

The last feature worth mentioning is Text Blocks, which allows programmers to deal with a multiline String in a much simpler way than before.