Aaron's Blog

https://aaronchenwei.github.io/

View on GitHub
15 November 2022

Java 17 vs Java 8 – the changes

by aaronchenwei

Spring Boot 3.0, the next major revision will be based on Spring Framework 6.0 and Java 17.

This release includes 135 enhancements, documentation improvements, dependency upgrades, and bug fixes. This is our first release candidate for Spring Boot 3.0, which we expect to release November 24, 2022.

Let’s have a quick review on what is new in Java 17.

1. New features in Java 17 in comparison with version 8

If you want to check a full list of changes to JDK, you would have already known that they are tracked as JEPs (JDK Enhancement Proposals). The full list can be found in JEP-0.

Also, if you want to compare Java APIs between versions, there is a great tool called Java Version Almanac. There were many useful, small additions to Java APIs, and checking this website is likely the best option if someone wants to learn about all these changes.

image

As for now, let’s review some important changes since Java 8.

1.1. New var keyword

A new var keyword was added that allows local variables to be declared in a more concise manner. Consider this code:

// java 8 way
Map<String, List<MyDtoType>> myMap = new HashMap<String, List<MyDtoType>>();
List<MyDomainObjectWithLongName> myList = aDelegate.fetchDomainObjects();

// java 10 way
var myMap = new HashMap<String, List<MyDtoType>>();
var myList = aDelegate.fetchDomainObjects()

When using var, the declaration it’s much, much shorter and, perhaps, a bit more readable than before. One must make sure to take the readability into account first, so in some cases, it may be wrong to hide the type from the programmer. Take care to name the variables properly.

Unfortunately, it is not possible to assign a lambda to a variable using var keyword:

// causes compilation error:
//   method reference needs an explicit target-type
var fun = MyObject::mySpecialFunction;

It is, however, possible to use the var in lambda expressions. Take a look at the example below:

boolean isThereAneedle = stringsList.stream()
  .anyMatch((@NonNull var s) -> s.equals(needle));

Using var in lambda arguments, we can add annotations to the arguments.

1.2. Records

One may say Records are Java’s answer to Lombok. At least partly, that is. Record is a type designed to store some data. Let me quote a fragment of JEP 395 that describes it well:

[…] a record acquires many standard members automatically:

  • A private final field for each component of the state description;
  • A public read accessor method for each component of the state description, with the same name and type as the component; A public constructor, whose signature is the same as the state description, which initializes each field from the corresponding argument; Implementations of equals and hashCode that say two records are equal if they are of the same type and contain the same state; and An implementation of toString that includes the string representation of all the record components, with their names.

In other words, it’s roughly equivalent to Lombok’s @Value. In terms of language, it’s kind of similar to an enum. However, instead of declaring possible values, you declare the fields. Java generates some code based on that declaration and is capable of handling it in a better, optimized way. Like enum, it can’t extend or be extended by other classes, but it can implement an interface and have static fields and methods. Contrary to an enum, a record can be instantiated with the new keyword.

A record may look like this:

record BankAccount (String bankName, String accountNumber) implements HasAccountNumber {}

And this is it. Pretty short. Short is good!

Any automatically generated methods can be declared manually by the programmer. A set of constructors can be also declared. Moreover, in constructors, all fields that are definitely unassigned are implicitly assigned to their corresponding constructor parameters. It means that the assignment can be skipped entirely in the constructor!

record BankAccount (String bankName, String accountNumber) implements HasAccountNumber {
  public BankAccount { // <-- this is the constructor! no () !
    if (accountNumber == null || accountNumber.length() != 26) {
      throw new ValidationException(Account number invalid);
    }
    // no assignment necessary here!
  }
}

For all the details like formal grammar, notes on usage and implementation, make sure to consult the JEP 359. You could also check a StackOverflow question on Java Records.

1.3. Extended switch expressions

Switch is present in a lot of languages, but over the years it got less and less useful because of the limitations it had. Other parts of Java grew, switch did not. Nowadays switch cases can be grouped much more easily and in a more readable manner (note there’s no break!) and the switch expression itself actually returns a result.

DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
boolean freeDay = switch (dayOfWeek) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> false;
    case SATURDAY, SUNDAY -> true;
};

Even more can be achieved with the new yield keyword that allows returning a value from inside a code block. It’s virtually a return that works from inside a case block and sets that value as a result of its switch. It can also accept an expression instead of a single value. Let’s take a look at an example:

DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
boolean freeDay = switch (dayOfWeek) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> {
      System.out.println("Work work work");
      yield false;
    }
    case SATURDAY, SUNDAY -> {
      System.out.println("Yey, a free day!");
      yield true;
    }
};

1.4. Instanceof pattern matching

While not a groundbreaking change, in my opinion, instanceof solves one of the more irritating problems with the Java language. Did you ever have to use such syntax?

if (obj instanceof MyObject) {
  MyObject myObject = (MyObject) obj;
  // … further logic
}

Now, you won’t have to. Java can now create a local variable inside the if, like this:

if (obj instanceof MyObject myObject) {
  // … the same logic
}

It is just one line removed, but it was a totally unnecessary line in terms of the code flow. Moreover, the declared variable can be used in the same if condition, like this:

if (obj instanceof MyObject myObject && myObject.isValid()) {
  // … the same logic
}

1.5. Sealed classes

This is a tricky one to explain. Let’s start with this – did the “no default” warning in switch ever annoy you? You covered all the options that the domain accepted, but still, the warning was there. Sealed classes let you get rid of such a warning for the instanceof type checks.

If you have a hierarchy like this:

public abstract sealed class Animal
    permits Dog, Cat {
}
public final class Dog extends Animal {
}
public final class Cat extends Animal {
}

You will now be able to do this:

if (animal instanceof Dog d) {
    return d.woof();
}
else if (animal instanceof Cat c) {
    return c.meow();
}

And you won’t get a warning. Well, let me rephrase that: if you get a warning with a similar sequence, that warning will be meaningful! And more information is always good.

I have mixed feelings about this change. Introducing a cyclic reference does not seem like a good practice. If I used this in my production code, I’d do my best to hide it somewhere deep and never show it to the outside world – I mean, never expose it through an API, not that I would be ashamed of using it in a valid situation.

1.6. TextBlocks

Declaring long strings does not often happen in Java programming, but when it does, it is tiresome and confusing. Java 13 came up with a fix for that, further improved in later releases. A multiline text block can now be declared as follows:

String myWallOfText = """
  ##     ##   #####   ####  #    #
 #  #   #  #  #    # #    # ##   #
#    # #    # #    # #    # # #  #
###### ###### #####  #    # #  # #
#    # #    # #   #  #    # #   ##
#    # #    # #    #  ####  #    #
"""

There is no need for escaping quotes or newlines. It is possible to escape a newline and keep the string a one-liner, like this:

String myPoem = """
Roses are red, violets are blue - \
Pretius makes the best software, that is always true
"""

Which is the equivalent of:

String myPoem = "Roses are red, violets are blue - Pretius makes the best software, that still is true";

Text blocks can be used to keep a reasonably readable json or xml template in your code. External files are still likely a better idea, but it’s still a nice option to do it in pure Java if necessary.

1.7. Better NullPointerExceptions

So, I had this chain of calls in my app once. And I think it may look familiar to you too:

String city = company.getOwner().getAddress().getCity();

And I got an NPE that told me precisely in which line the null was encountered. Yes, it was that line. Without a debugger I couldn’t tell which object was null, or rather, which invoke operation has actually caused the problem. Now the message will be specific and it’ll tell us that the JVM "cannot invoke Person.getAddress()".

Actually, this is more of a JVM change than a Java one – as the bytecode analysis to build the detailed message is performed at runtime JVM – but it does appeal to programmers a lot.

1.8. New HttpClient

There are many libraries that do the same thing, but it is nice to have a proper HTTP client in Java itself.

1.9. New Optional.orElseThrow() method

A get() method on Optional is used to get the value under the Optional. If there is no value, this method throws an exception. Like in the code below.

MyObject myObject = myList.stream()
  .filter(MyObject::someBoolean)
  .filter((b) -> false)
  .findFirst()
  .get();

Java 10 introduced a new method in Optional, called orElseThrow(). What does it do? Exactly the same! But consider the readability change for the programmer.

MyObject myObject = myList.stream()
  .filter(MyObject::someBoolean)
  .filter((b) -> false)
  .findFirst()
  .orElseThrow();

Now, the programmer knows exactly what will happen when the object is not found. In fact, using this method is recommended instead of the simple – albeit ubiquitous – get().

1.10. Other small but nice API changes

Talk is cheap, this is the code. In bold are the new things.

// invert a Predicate, will be even shorter with static import
collection.stream()
  .filter(Predicate.not(MyObject::isEmpty))
  .collect(Collectors.toList());

// String got some new stuff too
"\nPretius\n rules\n  all!".repeat(10).lines().
  .filter(Predictions.not(String::isBlank))
  .map(String::strip)
  .map(s -> s.indent(2))
  .collect(Collectors.toList());

// no need to have an instance of array passed as an argument
String[] myArray= aList.toArray(String[]::new);

// read and write to files quickly!
// remember to catch all the possible exceptions though
Path path = Files.writeString(myFile, "Pretius Rules All !");
String fileContent = Files.readString(path);

// .toList() on a stream()
String[] arr={"a", "b", "c"};
var list = Arrays.stream(arr).toList();

2. JVM changes

2.1. Project Jigsaw

JDK 9’s Project Jigsaw significantly altered the internals of JVM. It changed both JLS and JVMS, added several JEPs (list available in the Project Jigsaw link above), and, most importantly, introduced some breaking changes, alterations that were incompatible with previous Java versions.

2.2. Garbage Collectors

Note that in Java 8 you had much fewer options, and if you did not change your GC manually, you still used the Parallel GC. Simply switching to Java 17 may cause your application to work faster and have more consistent method run times. Switching to, then unavailable, ZGC or Shenandoah may give even better results.

2.2.1. G1 GC

As of Java 9, the G1 is the default garbage collector. It reduces the pause times in comparison with the Parallel GC, though it may have lower throughput overall. It has undergone some changes since it was made default, including the ability to return unused committed memory to the OS (JEP 346).

2.2.2. ZGC

A ZGC garbage collector has been introduced in Java 11 and has reached product state in Java 15 (JEP 377). It aims to reduce the pauses even further. As of Java 13, it’s also capable of returning unused committed memory to the OS (JEP 351).

2.2.3. Shenandoah GC

A Shenandoah GC has been introduced in JDK 14 and has reached product state in Java 15 (JEP 379). It aims to keep the pause times low and independent of the heap size.

2.2.4. No-Op GC

Finally, there’s a new No-Op Garbage Collector available (JEP 318), though it’s an experimental feature. This garbage collector does not actually do any work – thus allowing you to precisely measure your application’s memory usage. Useful, if you want to keep your memory operations throughput as low as possible.

2.3. Container awareness

In case you didn’t know, there was a time that Java was unaware that it was running in a container. It didn’t take into account the memory restrictions of a container and read available system memory instead. So, when you had a machine with 16 GB of RAM, set your container’s max memory to 1 GB, and had a Java application running on it, then often the application would fail as it would try to allocate more memory than was available on the container. A nice article from Carlos Sanchez explains this in more detail.

These problems are in the past now. As of Java 10, the container integration is enabled by default. However, this may not be a noticeable improvement for you, as the same change was introduced in Java 8 update 131, though it required enabling experimental options and using -XX:+UseCGroupMemoryLimitForHeap.

2.4. CDS Archives

In an effort to make the JVM start faster, the CDS Archives have undergone some changes in the time that passed since the Java 8 release. Starting from JDK 12, creating CDS Archives during the build process is enabled by default (JEP 341). An enhancement in JDK 13 (JEP 350) allowed the archives to be updated after each application run.

2.5. Java Flight Recorder and Java Mission Control

Java Flight Recorder (JEP 328) allows monitoring and profiling of a running Java application at a low (target 1%) performance cost. Java Mission Control allows ingesting and visualizing JFR data.

3. Should you migrate from Java 8 to Java 17?

TL;DR: Yes, you should.