VMware Cloud Services Technical

Using new Java features

Here at VMware’s Cloud Services Engagement Platform, we hold a high standard for the code we deliver to our customers. We strive to stay on top of cutting-edge solutions and also utilize proven technologies that have shaped the industry, like Java.

Java has seen its fair share of updates and improvements over the last couple of years, and it’s hard not to adopt some of the features during our development. The list is pretty extensive.

In this blog, I’ll be sharing some of the more useful features that Java’s instrumentation brought alongside releases 11 to 17.

Java 11 (LTS)

Java 11 is the first long-term support (LTS) release after Java 8. 

Release 10 was the last free Oracle JDK that we could use commercially without a license. Starting with Java 11, there’s no free long-term support (LTS) from Oracle.

This was the initial assumption of the JDK’s licensing, however, JDK 17 will be available free of charge for production use again – under the new Oracle No-Fee Terms and Conditions (NFTC) license.

Thankfully, Oracle continues to provide Open JDK releases, which we can download and use free of charge, regardless of the licencing changes by Oracle in the future.

String Class Changes

The String class in Java now supports new method implementations that prove useful when working with strings. 

String.isBlank() checks if a string is empty or only contains whitespace.

System.out.println("".isBlank());    // true
System.out.println(" ".isBlank());   // true
System.out.println("\n".isBlank());  // true

String.lines() returns a stream of strings, each string being on a new separate line.

"A\nB\nC\nD".lines()
    .filter(line -> "A".equals(line))
    .findFirst()
    .isPresent();  // true

" A\nB\nC\nD".lines()
    .filter(line -> "A".equals(line))
    .findFirst()
    .isPresent();  // false

String.strip() strips the whitespaces before and after string text.

String.stripLeading() and String.stripTrailing() could be used to remove whitespaces before and after string text respectfully.

System.out.println(" Hello world! ".strip());          // "Hello world!"
System.out.println(" Hello world! ".stripLeading());   // "Hello world! "
System.out.println(" Hello world! ".stripTrailing());  // " Hello world!"

Predicate.not() Method

Java’s Stream API is one of the most used functionality added with Java 8. Expanding on this, Java 11 brought some predicate negation that makes the code more readable.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

// Set up predicate to filter by
Predicate<Integer> isNumberLowerThanThree = x -> x < 3;

System.out.println(list.stream()
    .filter(isNumberLowerThanThree)
    .collect(Collectors.toList()));    // [1, 2]
 
// There is no need of a new negated predicate,
// just wrap the existing one and negate using Predicate.not()
System.out.println(list.stream()
    .filter(Predicate.not(isNumberLowerThanThree))
    .collect(Collectors.toList()));    // [3, 4, 5]

Local-Variable Syntax for Lambda Parameters

Allows var to be used when declaring the formal parameters of implicitly typed lambda expressions. Can alsо be paired with annotations such as @Nonnull.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

System.out.println(list.stream()
    .filter((var x) -> x % 2 == 0)
    .collect(Collectors.toList()));  // [2, 4]

System.out.println(list.stream()
    .filter((@Nonnull var x) -> x % 2 == 0)
    .collect(Collectors.toList()));  // [2, 4]

Java 12

Java 12 is the first release since the newly introduced 6-month release process after Java 11.

Most of the added features are improving upon the JVM, however, some useful tools were introduced with this release that might prove useful to the general audience.

Switch expressions were introduced as a preview feature and will be discussed later on in this blog at Java 14 when they are standardized. 

Teeing Collectors

Just like the tee command in Unix systems, a new static method .teeing() is added to the Stream Collectors API that enables merging two collectors at the end of the stream chain via BiFunction.

Each element is processed by both of the collectors and then merged with the specified function.

One example would be to get the average of a list of numbers. Here’s how to do it:

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
 
double average = list.stream()
    .collect(
        Collectors.teeing(
            Collectors.summingDouble(x -> x),
            Collectors.counting(),            
            (sum, count) -> sum / count));
 
System.out.println(average);           // 5.5

String Class Changes

The String class was updated in Java 12 with new methods that allow easier indentation and transformation of string objects. Such methods are String.indent(int) and String.transform(Function).

String.indent(int) indents the string it’s applied to with the given amount of spaces. 

System.out.println("Hello World!".indent(4));  // "    Hello World!"

String.transform(Function) transforms a string it’s applied to by the function passed as method argument.

String s = "Hello World!".transform(String::toUpperCase);
System.out.println(s);  // "HELLO WORLD!"

Java 13

With Java 13, two new useful features are now part of the experimental features. The first one is text blocks that allow multiline strings, used mostly for HTMLJSON, XML and other formats.

Switch expressions, which will be discussed in the following release, saw improvements with the addition of the newly introduced yield statement. 

Java 14

In Java 14, Switch expressions are now standardized. This means that they are no longer experimental features and can be used as part of the standard SDK. In this section, we will go through the changes of the switch statements

On the other hand, text blocks saw new improvements with escape sequences being added, however, this functionality is still under development and have not been redeemed to be standardized in this Java release.

Java Records are part of the experimental features of Java 14 along with Pattern Matching for instanceof operator.

Switch Expressions

The main idea of this feature is to extend the switch so it can be used as either a statement or an expression.

Consider the following switch that is a standard switch case:

switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

This switch statement can now be rewritten as an expression, like this:

switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

As an expression, the result of the switch case can be set to variables:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
};

The break with value statement is dropped in favour of the new yield statement in switch expressions:

int result = switch (s) {
    case "Foo":
        yield 1;
    case "Bar":
        yield 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        yield 0;
};

Quoting from the original JEP 361:

“The two statements, break (with or without a label) and yield, facilitate easy disambiguation betweenswitch statements and switch expressions: a switch statement but not a switch expression can bethe target of a break statement; and a switch expression but not a switch statement can be the target of a yield statement.”

Helpful NullPointerExceptions

Something new that was introduced directly to production without having to spend time under the label of a preview feature is the new Helpful NullPointerExceptions feature.

The new NullPointerExceptions generated by the JVM by describing precisely which variable was null, which was previously missing.

Consider the following scenario:

String a = null;
a.charAt(0);

This code fragment with result in the following NPE:

Exception in thread "main" java.lang.NullPointerException
    at com.vmware.java.blog.Main.main(Main.java:7)

With the new NPE, now we can see which variable exactly is null:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.charAt(int)" because "a" is null
    at com.vmware.java.blog.Main.main(Main.java:7)

Java 15

Java 15 finally saw text blocks being part of the standard SDK.

Furthermore, Records saw improvements and Sealed Classes, along with Hidden Classes are introduced as experimental features.

Text blocks

Coming with the Java 15 release, text blocks are available as a standard feature. For Java 13 and 14, we could use them in experimental mode with enabling the preview features.

This new standardized feature requires three double-quote marks “”” for the start of a new text block and also three double-quote marks for the end of one “””. With multiline text blocks, you don’t have to use a newline separator \n for each new line.

String JSON_EXAMPLE = """
    {
        "firstName" : "Mihail",
        "lastName" : "Hernandez",
        "company" : "VMWARE"
    }
""";

Java 16

Java 16 standardizes Records and Pattern matching instanceof, as well as improves upon Sealed/Hidden Classes.

Amongst other JVM improvements, the Stream API also saw new additions to its toolbox.

Pattern Matching for instanceof

Just like in The try-with-resources Statement, the pattern matching for instanceof functionality allows declaring a variable in the statement itself.

This shortens the code and makes it more readable.

Previously, when checking if an object is an instance of some class and cast it to the given class, we would do the following:

if (obj instanceof String) {
    String s = (String) obj;
    // do something with s
    s.charAt(5);
}

With pattern matching, we can write the code above as:

if (obj instanceof String s) {
    // Let pattern matching do the work!
    s.chatAt(5);
}

Records

Records enhance the Java programming language and are classes that act as transparent carriers for immutable data. They can be thought of as nominal tuples. JEP-395 finalizes this feature.

Creating a simple Point class would look something like this:

class Point {
   public double x;
   public double y;
}

With the introduction of Records, we can now create a new Record class of Point like this:

record Point(double x, double y) {}

This simple record also creates a parameterized constructor and overrides equals, hashCode, toString Object class methods. Serializable is also implemented by default by each record.

Here is an example app featuring records and their usage:

package com.vmware.java.blog;
 
public class Main {
    record Point(double x, double y) {
 
        // Point() {}

        // Non-canonical record constructor must 
        // delegate to another constructor
 
        Point() {
            this(0, 0);
        }
 
        @Override
        public double x() {
            System.out.println(
                String.format("Accessing x value %s.", x));
            return x;
        }
    }
 
    public static void main(String[] args) {
        Point p = new Point();
        System.out.println(p);        // Point[x=0.0, y=0.0]
 
        Point p2 = new Point(1, 2);
        System.out.println(p2);       // Point[x=1.0, y=2.0]
 
        Point p3 = new Point(1, 2);
        System.out.println(p3);       // Point[x=1.0, y=2.0]

        // This won't compile as records are deemed immutable 
        // p.x(1);

        System.out.println(p2.x);     // 1.0
        System.out.println(p2.x());   // Accessing x value 1.0.
                                      // 1.0
         
        System.out.println(p2.equals(p3)); // true
    }
}

As you can see, fields are public and can be accessed directly, however, you can call the existing assessor methods and override them if needed. Default constructor can be created 

The standard convention in Java tells us to use the get prefix word for accessor methods, however, this is omitted in records. 

Stream.toList Method

A new method toList() was added in the Stream class in Java 16 with the idea of reducing boilerplate code when using collect.(Collectors.toList());

List<Integer> intList = Arrays.asList("1","2","3")
                              .stream()
                              .map(Integer::parseInt)
                              .toList();

Java 17 (LTS)

Java 17 will be the new long-term support (LTS) release of the standard Java. The last LTS release was JDK 11, which was three years ago.

With this new Java release being LTS, more and more Java users are expected to transition to the latest LTS version of the JDK and thus, inherit all of the standardized and production-ready features from the previous LTS release.

Among many new features and improvements, we will take a look at the standardization of Sealed Classes and what they might be used for, as well as Pattern Matching for Switch Statements that is still in preview, much like the Pattern Matching for instanceof.

Sealed Classes

Sealed classes saw two preview releases, respectively in Java 15 and 16. However, in the newest LTS version, sealed classes are now production-ready and standardized.

The main idea of the sealed classes feature is to allow the author of a class or interface to control which code is responsible for implementing it.

A sealed class or interface can be extended or implemented only by those classes and interfaces permitted to do so. A class is sealed by applying the sealed modifier to its declaration.

Then, after any extends and implements clauses, the permits clause specifies the classes that are permitted to extend the sealed class.

New keywords here are “sealed“, “non-sealed” and “permits“:

Take the following code snippet as an example:

package com.vmware.java.blog;

// A definition of a sealed class, that permits Circle, Rectangle,
// and Square classes to extend this sealed class.

public abstract sealed class Shape permits Circle, Rectangle, Square {}

And now the child class:

package com.vmware.java.blog;
 
// final/sealed/non-sealed can be applied 
// as modifiers here instead of non-sealed in the example

public non-sealed class Circle extends Shape {}

The child class has now three options and this applies recursively to each child class of a child class that ultimately is a child of the initial sealed class:

  1. Be final → Child class cannot be extended. (Closes the hierarchy)
  2. Be sealed → Child class is sealed and needs to permit which classes are going to inherit it. (Extends the hierarchy)
  3. Be non-sealed → Child class is not sealed and thus doesn’t need to permit which classes are going to inherit it. (Could still be inherited and doesn’t close the hierarchy)

Pattern Matching for Switch Statements (preview)

Pattern matching for switch statements acts like the pattern matching functionality for instanceof.

Code examples here are taken from the official Oracle Docs page.

Consider the following scenario:

interface Shape { }
record Rectangle(double length, double width) implements Shape { }
record Circle(double radius) implements Shape { }
...
    public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        if (shape instanceof Rectangle r) {
            return 2 * r.length() + 2 * r.width();
        } else if (shape instanceof Circle c) {
            return 2 * c.radius() * Math.PI;
        } else {
            throw new IllegalArgumentException("Unrecognized shape");
        }
    }

When trying to get the perimeter of the Shape, we want to check first what is the type of the Shape object. This can be accomplished using pattern matching functionality for instanceof operator, like in the example above.

Pattern matching for switch statements feature allows us to use a switch statement and pattern matching at the same time to get things done in a much more readability-friendly manner:

public static double getPerimeter(Shape shape) throws IllegalArgumentException {
    return switch (shape) {
        case Rectangle r -> 2 * r.length() + 2 * r.width();
        case Circle c    -> 2 * c.radius() * Math.PI;
        default          -> throw new IllegalArgumentException("Unrecognized shape");
    };
}