Automation QA Testing Course Content

Java 17 vs Java 11: Exploring the Latest Features and Improvements

 Java 17 is the latest LTS(Long Term Support) version of the Java programming language, released on September 14, 2021. If you’re currently using Java 11, it may be time to consider migrating to Java 17 to take advantage of its new features and improvements. In this article, we’ll discuss what’s new in Java 17, although some of the features discussed here have been introduced in version following up to Java 17 from Java 11.

Why should we move from Java 11?

Although Java 11 is also an LTS version and is used by many application, there are some major reasons why we might want to shift to Java 17.

  1. Ending Support for Java 11: Java 11 will be supported till September 2023 and extended support will be provided till September 2026. This means that after the support ends, we would have no patches(not even the security ones).
  2. Spring 6: The latest version of Spring, Spring 6 will require Java 17 to work, and as there are many libraries which work along with them, they would also be moving to Java 17. If your applications rely on the Spring Framework, you should definitely consider moving to Java 17.
  3. Free Oracle JDK present for Java 17: Java 17 is issued under the new NFTC (Oracle No-Fee Terms and Conditions) license. It is therefore again allowed to use the Oracle JDK version for free for production and commercial use.(was not allowed for Java 11).

Whats new in Java 17?

With Java 17 , there have been several improvements and new features introduced which will have long term support.

Text Blocks

Java has introduced text blocks to make the code more readable and to avoid unnecessary string formatting. Now, we can place our text in between the triple quotes and have multiple double quoted strings inside it without having to use escape characters. An example is shown below:

private static void jsonBlock() {
String text = """
{
"name": "John Doe",
"age": 45,
"address": "Doe Street, 23, Java Town"
}
"""
;
System.out.println(text);
}

As we can see, this makes it very easy to write Json and similar string which would require heavy usage of escape characters.

Also, the ending three double quotes indicate the beginning of the Text Block or its indentation in the output. In the example above, there will be two spaces for each line in the output as the position of the double quotes are two spaces behind the last character.

Two new escape characters have been introduced for use inside text blocks, ‘\s’ for adding a space and ‘\’ for removing newline. Especially useful when writing long SQL statements.

private static void sqlStatement() {
String sql = """
SELECT id, firstName, lastName\s\
FROM Employee
WHERE departmentId = "IT" \
ORDER BY lastName, firstName"""
;
System.out.println(text);
}

Improved Switch Statements

Switch Expressions will allow you to return values from the switch case and use these return values in assignments, etc. Java allows use of operator ->(arrow) instead of : (colon) to denote the return expression. break keyword will not be needed when returning using switch in this expression, but default case is required.

private static void improvedSwitch(Fruit fruit) {
String text = switch (fruit) {
case APPLE, PEAR -> {
System.out.println("the given fruit was: " + fruit);
yield "Common fruit";
}
case ORANGE, AVOCADO -> "Exotic fruit";
default -> "Undefined fruit";
};
System.out.println(text);
}

In case of multiple operations done inside a switch case, we can have a case block and denote the return value using the yield keyword. yield here is a context dependent keyword i.e. you can have a variable name yield somewhere else inside the function.

record’ Type

record classes are a special kind of immutable class which is meant to replace data transfer objects(DTOs). Normally if we want to use some POJO inside our class or methods, we would have to declare the class along with defining all the getters, setters, equals and hashcode functions. For example to use a sample Fruit class in other places, we would have to define our class someway like below:

public class Fruit {
private String name;
private int price;

//getters, setters, equals and hashcode methods
}

Although we can reduce most of our boilerplate code by using libraries like lombok, we can still reduce it even further with the help of records. With records the same code becomes:

public static void doSomething() {
record Fruit(String name, int price) {}
Fruit fruit = new Fruit("Apple", 100);
System.out.println(fruit.getPrice());
}

As we can see, we can even define method local record objects. The records object automatically provides us with getter, equals and hashcode methods for all its fields.

The fields inside the record cannot be changed, and it can only be defined by the arguments given when declaring the record as shown above(but we can define static variables). We can also define a custom constructor which can validate the fields. It is recommended that we do not override the getters and setters of records which could affects its immutability. An example of a record with multiple constructors and static variables and methods is shown below:

public record Employee(int id, String firstName,
String lastName)

{

static int empToken;

// Compact Constructor
public Employee
{
if (id < 100) {
throw new IllegalArgumentException(
"Employee Id cannot be below 100.");
}
if (firstName.length() < 2) {
throw new IllegalArgumentException(
"First name must be 2 characters or more.");
}
}


// Alternative Constructor
public Employee(int id, String firstName)
{
this(id, firstName, null);
}

// Instance methods
public void getFullName()
{
if (lastName == null)
System.out.println(firstName());

else
System.out.println(firstName() + " "
+ lastName());
}

// Static methods
public static int generateEmployeeToken()
{
return ++empToken;
}
}

Some more qualities of record classes are:
1. You can use nested classes and interfaces inside a record.

2. You can have nested records too, which will implicitly be static.

3. A record can implement interfaces.

4. You can create a generic record class.

5. Records are serializable.

More on records can be found here:

‘sealed’ Classes

sealed class will give us more control over which classes are allowed to extend our classes. In Java 11, a class can be final or extended. If you want to control which classes can extend your super class, you can put all classes in the same package and you give the super class package visibility. However, it is not possible anymore to access the super class from outside the package. As an example, see the code below:

public abstract class Fruit {
}
public final class Apple extends Fruit {
}
public final class Pear extends Fruit {
}
private static void problemSpace() {
Apple apple = new Apple();
Pear pear = new Pear();
Fruit fruit = apple;
class Avocado extends Fruit {};
}

Here, we cannot stop Avocado to extend the Fruit class. If we make the Fruit class default, then the assignment of apple to fruit object would not compile. Hence, now we can use sealed classes to allow only specific classes to extend our superclass. An example is given below:

public abstract sealed class FruitSealed permits AppleSealed, PearSealed {
}
public non-sealed class AppleSealed extends FruitSealed {
}
public final class PearSealed extends FruitSealed {
}

As we see, we use a new keyword sealed to denote that this is a sealed class. We define the classes that can be extended using the permits keyword. Any class which extends the sealed class can be either final like PearSealed or can be extended by other classes by using the non-sealed keyword when declaring the class as with AppleSealed.

This implementation would allow AppleSealed to be assigned to FruitSealed class but wont allow any other classes not defined by permits keyword to extend FruitSealed class. More on sealed classes here.

Pattern Matching with ‘instance of’

In Java 11, we usually use the instance of operator to check whether an object belongs to a certain class. We need to explicitly cast the object to that particular class if we want to perform some operation on it once the instance of check returns true. An example is shown below:

private static void oldStyle() {
Object o = new Grape(Color.BLUE, 2);
if (o instanceof Grape) {
Grape grape = (Grape) o;
System.out.println("This grape has " + grape.getPits() + " pits.");
}
}

Here, we needed to explicitly cast the object to type Grape and then find out the number of pits. With Java 17, we can change this to:

private static void patternMatchingInJava17() {
Object o = new Grape(Color.BLUE, 2);
if (o instanceof Grape grape) {
System.out.println("This grape has " + grape.getPits() + " pits.");
}
}

We can pair the instance of check with an &&(and) condition but not ||(or) as in case of an “or” condition the statement can reach the other condition even if instance of check return false

The scope of the variable grape can even extend beyond the if block if the instance of check return true. In the example below, Runtime Exception will be thrown if the object is not of Grape type, hence the compiler will know for sure that the grape object should exist when it reaches the print statement. More on pattern matching with instance of can be found here.

private static void patternMatchingScopeException() {
Object o = new Grape(Color.BLUE, 2);
if (!(o instanceof Grape grape)) {
throw new RuntimeException();
}
System.out.println("This grape has " + grape.getPits() + " pits.");
}

Helpful NullPointerException

In Java 11 , when we get a NullPointerException, we only get the line number on which the exception occurred but we don’t get the method or variable which resolved to null. With Java 17, the messaging has been improved as the NullPointerException message also tells us the exact method invocation which caused the NullPointerException.

public static void main(String[] args) {
HashMap<String, Grape> grapes = new HashMap<>();
grapes.put("grape1", new GrapeClass(Color.BLUE, 2));
grapes.put("grape2", new GrapeClass(Color.white, 4));
grapes.put("grape3", null);
var color = ((Grape) grapes.get("grape3")).getColor();
}

As we can see here, we are trying to get the colour of a “grape3” object which is null. When we compare the error message we get in Java 11 and Java 17, we see the difference in the error messaging as now we get to know exactly that calling the get method on the null object present in the map caused the exception.

// Java 11
Exception in thread "main" java.lang.NullPointerException
at com.rg.java17.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)
// Java 17
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "com.rg.java17.Grape.getColor()" because the return value of "java.util.HashMap.get(Object)" is null
at com.rg.java17.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:13)

Some more improvements

Compact Number Formatting Support

A factory method is added to NumberFormat class in order to format numbers in compact, human-readable form according to the Unicode standard. There is a SHORT and LONG format available, an example is shown below:

NumberFormat shortFormat = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);
System.out.println(shortFormat.format(1000))

NumberFormat longFormat = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);
System.out.println(shortFormat.format(1000))
// Output
1K
1 thousand

Day Period Support Added

A new pattern “B” is added to DateTime pattern allowing it to specify the time of the day.

DateTimeFormatter timeOfDayFomatter = DateTimeFormatter.ofPattern("B");
System.out.println(timeOfDayFomatter.format(LocalTime.of(8, 0)));
System.out.println(timeOfDayFomatter.format(LocalTime.of(13, 0)));
System.out.println(timeOfDayFomatter.format(LocalTime.of(20, 0)));
System.out.println(timeOfDayFomatter.format(LocalTime.of(23, 0)));
System.out.println(timeOfDayFomatter.format(LocalTime.of(0, 0)));
// Output
in the morning
in the afternoon
in the evening
at night
midnight

Performance Benchmarks

Java 17 has also shown improvement over Java 11 in terms of its memory usage and time complexity. One such benchmark has been done where they tally code written in both version on their performance by making them do a series of tasks. The complete results and task descriptions can be found here.

Some of the general results which have been noted are:

  1. Java 17 is 8.66% faster than Java 11 and 2.41% faster than Java 16 for G1GC (default garbage collector).

2. Java 17 is 6.54% faster than Java 11 and 0.37% faster than Java 16 for ParallelGC(Parallel Garbage Collector).

3. The Parallel Garbage Collector(Available in Java 17) is 16.39% faster than the G1 Garbage Collector(Used in Java 11).

Migrating from Java 11 to Java 17 can provide many benefits, including new features and improved performance. However, it’s essential to be aware of potential bottlenecks that may arise during the migration process. Many libraries would also be upgrading to newer versions to support Java 17. Hence we should be extremely careful if we are using external libraries in our projects

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.