Java quietly underwent one of the biggest changes in its development in 2018 with the adoption of a new release cadence. This bold new plan resulted in Java developers getting a new feature release every six months.

This is wonderful for keeping Java fresh and relevant, but it makes it pretty easy to miss features as they are introduced. This article rounds up several useful new features and gives an overview of them.

The Optional class

The null pointer exception is one of the most classic of all errors. And while it may be familiar, it is a very verbose problem to guard against. At least it was until Java 8 introduced (and Java 10 refined) the Optional class.

In essence, the Optional class allows you to wrap a variable, and then use the wrapper’s methods to deal more succinctly with nullness.

Listing 1 has an example of a garden variety null pointer error, wherein a class reference, foo, is null and a method, foo.getName(), is accessed on it.

Listing 1. Null pointer without Optional

public class MyClass {
    public static void main(String args[]) {
      InnerClass foo = null;
      System.out.println("foo = " + foo.getName());
    }
}
class InnerClass {
  String name = "";
  public String getName(){
      return this.name;
  }
}

Optional provides a number of approaches for dealing with such situations, depending on your needs. It sports an isPresent() method that you can use to do an if-check. That ends up being fairly verbose, however. But Optional also has methods for functional handling. For example, Listing 2 shows how you could use ifPresent() — notice the the one letter difference to isPresent() — to only run the output code if there is a value present.

Listing 2. Only run code if value is present

import java.util.Optional;
public class MyClass {
    public static void main(String args[]) {
      InnerClass foo = null; //new InnerClass("Test");
      Optional fooWrapper = Optional.ofNullable(foo);
      fooWrapper.ifPresent(x -> System.out.println("foo = " + x.getName()));
      //System.out.println("foo = " + fooWrapper.orElseThrow());
    }
}
class InnerClass {
  String name = "";
  public InnerClass(String name){
      this.name = name;
  }
  public String getName(){
      return this.name;
  }
}

A tip: When using Optional, if you use the orElse() method to provide a default value via method call, consider using orElseGet() to provide a function reference instead, to reap the performance benefits of not running the call if the value is non-null.

The Record class (preview feature)

A common need in building Java apps is what’s referred to as an immutable DTO (Data Transfer Object). DTOs are used for modeling the data from databases, file systems, and other data stores. Traditionally, DTOs are created by making a class whose members are set via constructor, without getters to access them. Java 14 introduced, and Java 15 improved upon, the new record keyword that provides a shorthand for this very purpose.

Listing 3 illustrates a typical DTO definition and usage before the record type was introduced.

Listing 3. A simple immutable DTO

public class MyClass {
    public static void main(String args[]) {
      Pet myPet = new Pet("Sheba", 10);

      System.out.println(String.format("My pet %s is aged %s", myPet.getName(), myPet.getAge()));
    }
}
class Pet {
    String name;
    Integer age;
    public Pet(String name, Integer age){
        this.name = name;
        this.age = age;
    }
    public String getName(){
        return this.name;
    }
    public Integer getAge(){
        return this.age;
    }
}

We can eliminate much of the boilerplate using the record keyword as shown in Listing 4.

Listing 4. Using the record keyword

public class MyClass {
    public static void main(String args[]) {
      Pet myPet = new Pet("Sheba", 10);

      System.out.println(String.format("My pet %s is aged %s", myPet.getName(), myPet.getAge()));
    }
}

public record Pet(String name, Integer age) {}

Notice that the client code that makes use of the data object doesn’t change; it behaves just like a traditionally defined object. The record keyword is smart enough to infer what fields exist via the simple definition footprint.

The record type also defines default implementations for equals(), hashCode(), and toString(), while also allowing for these to be overridden by the developer.  You can also provide a custom constructor.

Note that records cannot be subclassed.

New String methods

In Java 10 and Java 12, several useful new String methods were added. In addition to string manipulation methods, two new methods for simplifying text file access were introduced.

The new String methods in Java 10:

  • isBlank(): Returns true if the string is empty or the string contains only white space (this includes tabs). Note isBlank() is distinct from isEmpty(), which returns true only if length is 0.
  • lines(): Splits a string into a stream of strings, each string containing a line.  Lines are defined by /r or /n or /r/n. As an example, consider Listing 5 below.
  • strip(), stripLeading(), stripTrailing(): Removes white space from beginning and ending, beginning only, and ending only, respectively.
  • repeat(int times): Returns a string that takes the original string and repeats it the specified number of times.
  • readString(): Allows for reading from a file path directly to a string as seen in Listing 6.
  • writeString(Path path): Writes the string directly to the file at the specified path.

The new String methods in Java 12:

  • indent(int level): Indents the string the specified amount. Negative values will only affect leading white space.
  • transform(Function f): Applies the given lambda to the string.

Listing 5. String.lines() example

import java.io.IOException;
import java.util.stream.Stream;
public class MyClass {
    public static void main(String args[]) throws IOException{
      String str = "test \ntest2 \n\rtest3 \r";
      Stream lines = str.lines();
      lines.forEach(System.out::println);
    }
}

/*
outputs:
test
test2
test3
*/

Listing 6. String.readString(Path path) example

Path path = Path.of("myFile.txt"); 
String text = Files.readString(path);
System.out.println(text);

Switch expressions

Java 12 introduced the switch expression which allows for switch to be used inline within a statement. In other words, the switch expression returns a value. Java 12 also provides for an arrow syntax that eliminates the need for an explicit break to prevent fallthrough. Java 13 went a step further and introduced the yield keyword to explicitly denote what value the switch case returns. Java 14 adopted the new switch expression syntax as a full feature.

Let’s look at some examples. First, Listing 7 has a (very contrived) example of a switch statement in the traditional (Java 8) format. This code uses a variable (message) to output the name of a number if it is known.

Listing 7. Old-fashioned Java switch

class Main { 
  public static void main(String args[]) {
    int size = 3;
    String message = "";

switch (size){
 case 1 :
message = "one";
 case 3 :
   message = "three";
break;
 default :
message = "unknown";
break;
}

System.out.println("The number is " + message);
  }
}

Now this code is quite verbose and finicky. In fact, there is already an error in it! Look closely for a missing break. Listing 8 simplifies it by using a switch expression.

Listing 8. New switch expression

class NewSwitch { 
  public static void main(String args[]) {
    int size = 3;

    System.out.println("The number is " +
      switch (size) {
        case 1 -> "one";
        case 3 -> "three";
        default -> "unknown";
      }
    );
  }
}

In Listing 8, you can see that the switch expression goes right inside the System.out.println call. That is already a big readability win, and nukes the superflous message variable. Also, the arrow syntax reduces the code footprint by eliminating the break statement. (The yield keyword is used when not using the arrow syntax.)

Learn more about the new switch expression syntax here.

Text blocks

Java 13 addresses a long-standing annoyance in dealing with complex text strings in Java by introducing the text block. Java 14 refined this support.

Things like JSON, XML, and SQL can drive you crazy with multiple nested layers of escaping. As the spec explains:

In Java, embedding a snippet of HTML, XML, SQL, or JSON in a string literal … usually requires significant editing with escapes and concatenation before the code containing the snippet will compile. The snippet is often difficult to read and arduous to maintain.

Take a look at Listing 9, wherein the new text block syntax is used to create a JSON snippet.

Listing 9. JSON using a text block

class TextBlock { 
  public static void main(String args[]) {
    String json = """
      {
        "animal" : "Quokka",
        "link" : "https://en.wikipedia.org/wiki/Quokka"
      }
    """;

    System.out.println(json);
  }
}

In Listing 9, there is not an escape character in sight. Notice the triple double-quote syntax.

Sealed classes

Java 15 (JEP 260) introduces the idea of a sealed class. In short, the new sealed keyword allows you to define what classes can subclass an interface. An example is worth a thousand words in this case. See Listing 10.

Listing 10. Sealed class example

public abstract sealed class Pet
    permits Cat, Dog, Quokka {...}

Here the interface designer uses the sealed keyword to specify which classes are permitted to extend the Pet class.

Overall, it’s apparent that the new approach to Java releases is working. We are seeing a lot of new ideas making their way through the JEP (JDK Enhancement Proposal) process into actual, useable Java features. This is wonderful news for Java developers. It means we are working in a living, evolving language and platform.

Source