JDK New Language Syntaxes (JDK 7 to JDK 21)

References:
  1. "Java Language Updates Release 21" @ https://docs.oracle.com/en/java/javase/21/language/index.html - describes the updated language features from JDK 9 to JDK 21.
Preview Features

Many of the new features started as preview features, and are standardized in later releases. To compile and run a program with preview features for JDK 21, you need to include the --enable-preview -source 21 flags. For example,

// To compile with preview feature for JDK 21
javac --enable-preview -source 21 HelloPreview.java
// To run
javac --enable-preview HelloPreview

Summary of Language Change from JDK 9

(JDK 7,12-14) "switch" Enhancements

  • (JDK 7) Switch with String selector
  • (JDK 12) JEP 325: Switch Expressions (Preview)
  • (JDK 13) JEP 354: Switch Expressions (Second Preview)
  • (JDK 14) JEP 361: Switch Expressions (Standard)
  • (JDK 17) JEP 406: Pattern Matching for switch (Preview) [TODO]
  • (JDK 18) JEP 420: Pattern Matching for switch (Second Preview) [TODO]
  • (JDK 19) JEP 427: Pattern Matching for switch (Third Preview) [TODO]
  • (JDK 20) JEP 433: Pattern Matching for switch (Fourth Preview) [TODO]
  • (JDK 21) JEP 441: Pattern Matching for switch (Standard) [TODO]
(JDK 7) switch with String selector

Prior to JDK 7, a switch works on integral primitive (byte, short, int, char; but NOT long). It also works with enumerated types (Enum) and primitive wrapper classes (Character, Byte, Short, and Integer).

From JDK 7, you can use a String object as the selector. For example,

public class J7SwitchOnStringTest {
   public static void main(String[] args) {
      String day = "SAT";
      switch (day) {  // switch on String selector (JDK 7)
         case "MON": case "TUE": case "WED": case "THU":
            System.out.println("Working Day");
            break;
         case "FRI":
            System.out.println("Thank God It's Friday");
            break;
         case "SAT": case "SUN":
            System.out.println("Gone Fishing");
            break;
         default:
            System.out.println("Invalid");
            break;  // recommended good defensive code
      }
   }
}

Notes:

  • String's .equals() method, which is case-sensitive, is used in comparison.
  • switch vs. nested-if: An if-then-else statement can test expressions based on ranges of values or conditions (e.g., num!=-1, x+y<5), whereas a switch statement tests expressions based only on a single integer, enumerated value, or String object.
  • You probably should use an Enum, which is safer, in this example.

"switch on String" is handy in handling options specified in command-line arguments, which are Strings. For example,

/**
 * This program accepts the following command-line options:
 *   -c|--create  : create
 *   -v|--verbose : verbose
 *   -d|--debug   : debug
 * More than one options can be specified in any order.
 */
public class J7SwitchArgsTest {
   public static void main(String[] args) {
      boolean create  = false;
      boolean verbose = false;
      boolean debug   = false;

      for (String arg : args) {
         switch (arg) {   // Switch on String selector (JDK 7)
            case "-c": case "--create":
               create = true;
               break;
            case "-v": case "--verbose":
               verbose = true;
               break;
            case "-d": case "--debug":
               debug = true;
               break;
            default:
               throw new IllegalArgumentException("invalid option " + arg);
         }
      }
      System.out.println("create: " + create);
      System.out.println("verbose: " + verbose);
      System.out.println("debug: " + debug);  
   }
}
(JDK 12, 13, 14) "switch" expressions

"switch expressions" started in JDK 12 (JEP 325) as a preview feature and continued to JDK 13 (JEP354) for second review. It is standardized in JDK 14 (JEP 361).

The original switch statement (follows the C/C++) has several irregularities, such as:

  • the default control flow behavior between switch labels (i.e., fall through without a break statement)
  • switch works only as a statement, not as an expression (i.e., switch does not return a value)
  • the default scoping in switch block (the whole block is treated as one scope)
New Arrow Labels "case L ->" and New "yield" Statement
  • The original switch's label has the form of "case L:" which fall through without break. JDK 14 introduced arrow labels, in the form of "case L ->", which does not fall thru to the next case and hence break is not needed.
  • switch in arrow-label form can take an expression that evaluates to a value.
  • JDK 14 also introduced a new yield statement to produce a value.

For example:

enum Day {
   SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;
}

public class J14SwitchExprYieldTest {
   public static void main(String[] args) {
      Day day = Day.SUNDAY; // switch on String selector (From JDK 7)
      int numLetters;

      // Before JDK 14, switch is a statement without evaluated value
      switch (day) {
         case MONDAY: case FRIDAY: case SUNDAY: numLetters = 6; break;
         case TUESDAY:                          numLetters = 7; break;
         case THURSDAY: case SATURDAY:          numLetters = 8; break;
         case WEDNESDAY:                        numLetters = 9; break;
         default:  // defensive code for enum
            numLetters = 0;
            throw new IllegalArgumentException("invalid day " + day);
      }
      System.out.println("1. Number of letters: " + numLetters);

      // JDK 14 supports switch expression that evaluates to a value
      numLetters =   // Assign the new switch expression to a variable
         switch (day) {
            case MONDAY, FRIDAY, SUNDAY -> 6; // single-line expression
            case TUESDAY                -> 7;
            case THURSDAY, SATURDAY     -> 8;
            case WEDNESDAY              -> 9;
            default -> { // braces {} needed for block in 'case -> L', local scope
               System.out.println("invalid day " + day);
               yield 0; // use "yield" to return a value in a block
            }
         };
      System.out.println("2. Number of letters: " + numLetters);

      // switch expression can also use the traditional "case L:" with yield
      numLetters = switch (day) {
         case MONDAY: case FRIDAY: case SUNDAY: yield 6;
         case TUESDAY:                          yield 7;
         case THURSDAY: case SATURDAY:          yield 8;
         case WEDNESDAY:                        yield 9;
         default: // braces { } NOT needed in 'case: L'
            System.out.println("invalid day " + day);
            yield 0; // use "yield" to return a value in a block
      };
      System.out.println("3. Number of letters: " + numLetters);
   }
}

Notes:

  • Multiple labels are separated by commas in arrow labels "case L ->".
  • Body blocks must be enclosed in braces, with its scope.
(JDK 17,18,19,20,21) Pattern Matching for switch

[TODO]

(JDK 7) Binary Integer Literals with Prefix "0b" and Underscore (_) in Numeric Literals

From JDK 7, you can express literal values in binary with prefix '0b' (zero-b) (or '0B') for integral types, similar to C/C++. Prior to JDK 7, you can only use octal literal values with prefix '0' (zero) or hexadecimal literal values with prefix '0x' (zero-x) or '0X'.

From JDK 7, you are also permitted to use underscore (_) to break the digits to improve the readability but you must start and end with a digit. Underscores are allowed in fractional part for floating point numbers too. For examples,

int anInt1 = 0b01010000101000101101000010100010;    // binary literal prefix with '0b' or '0B'
int anInt2 = 0b0101_0000_1010_0010_1101_0000_1010_0010;  // break digits with underscore for readability
int anInt3 = 2_123_456;  // break the digits with underscore
 
byte aByte = (byte)0b0110_1101;  // '0b' for int(32-bit), need to cast to byte(8-bit)/short(16-bit)
short aShort = (short)0b0111_0101_0000_0101;
long aLong = 0b1000_0101_0001_0110_1000_0101_0000_1010_0010_1101_0100_0101_1010_0001_0100_0101L;
      // long(64-bit) with suffix 'L'
 
double aDouble = 3.1415_9265;  // You can also use underscore on fractional part for readability
float aFloat = 3.14_15_92_65f;

(JDK 7) Catching Multiple Exception Types and Re-throwing Exceptions with Improved Type Checking

Prior to JDK 7, you need two catch blocks to catch two exception types even though both perform identical task. For example,

try {
   ......
} catch(ClassNotFoundException ex) {
   ex.printStackTrace();
} catch(SQLException ex) {
   ex.printStackTrace();  // same actions
}

From JDK 7, you could use one single catch block to handle more than one exception types, with the exception types separated by a vertical bar (|). For example,

try {
   ......
} catch(ClassNotFoundException|SQLException ex) {
   ex.printStackTrace();
}

(JDK 7,9) Automatic Resource Management in try-with-resources Statement

(Pre-JDK 7) try-catch-finally

Prior to JDK 7, we have the try-catch-finally clause to handle exceptions. For example,

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;

// Copy from one file to another file line by line.
// Pre-JDK 7 requires you to close the resources using a finally block.
public class PreJ7FileCopy {
   public static void main(String[] args) {
      BufferedReader src = null;
      BufferedWriter dest = null;
      try {
         src = new BufferedReader(new FileReader("in.txt"));
         dest = new BufferedWriter(new FileWriter("out.txt"));
         String line;
         while ((line = src.readLine()) != null) {
            System.out.println(line);
            dest.write(line);
            dest.newLine(); // write a newline
         }
      } catch (IOException ex) {
         ex.printStackTrace();
      } finally { // always close the streams
         try {
            if (src != null)
               src.close(); // close() throw IOException
            if (dest != null)
               dest.close();
         } catch (IOException ex) {
            ex.printStackTrace();
         }
      }
   }
}
(JDK 7) New try-with-resources Statement in the form of "try(resources)"

JDK 7 introduces a try-with-resources statement, which ensures that each of the resources in try(resources) is properly closed at the end of the statement. This results in much cleaner codes.

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;

// Copy from one file to another file line by line.
// JDK 7 has a try-with-resources statement, which ensures that
//   each resource opened in try(resources) is closed at the end of the statement.
public class J7FileCopy {
   public static void main(String[] args) {
      try (BufferedReader src = new BufferedReader(new FileReader("in.txt"));
            BufferedWriter dest = new BufferedWriter(new FileWriter("out.txt"))) {  // JDK 7
         String line;
         while ((line = src.readLine()) != null) {
            System.out.println(line);
            dest.write(line);
            dest.newLine();
         }
      } catch (IOException ex) {
         ex.printStackTrace();
      }
      // src and dest automatically close.
      // No need for finally to explicitly close the resources.
   }
}
(JDK 9) try-with-resource on final or "effectively final" variables

From JDK 9, if you already have a resource as a final or "effectively final" variable, you can use that variable in a try(resources) clause without declaring a new variable. An "effectively final" variable is one whose value is never changed after it is initialized. For example,

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;

// Copy from one file to another file line by line.
// Java 7 has a try-with-resources statement, which ensures that
//   each resource opened in try(resources) is closed at the end of the statement.
public class J9FileCopy {
   public static void main(String[] args) throws IOException {
      BufferedReader src = new BufferedReader(new FileReader("in.txt"));
      BufferedWriter dest = new BufferedWriter(new FileWriter("out.txt"));

// before JDK 9
//    try (BufferedReader src1 = src; BufferedWriter dest1 = dest) {  // local variables only
      try (src; dest) {  // final or effectively final variables permitted from JDK 9
         String line;
         while ((line = src.readLine()) != null) {
            System.out.println(line);
            dest.write(line);
            dest.newLine();
         }
      } catch (IOException ex) {
         ex.printStackTrace();
      }
      // src and dest automatically close.
      // No need for finally to explicitly close the resources.
   }
}

(JDK 7,9) Improved Type Inference for Generic Instance Creation with the Diamond Operator <>

(JDK 7) New Diamond Operator <>

For example,

import java.util.*;

public class J7GenericTest {
   public static void main(String[] args) {
      // Pre-JDK 7
      List<String> lst1 = new ArrayList<String>();
      // JDK 7 supports limited type inference for generic instance creation with
      // diamond operator <>
      List<String> lst2 = new ArrayList<>();

      lst1.add("Mon");
      lst1.add("Tue");
      lst2.add("Wed");
      lst2.add("Thu");

      for (String item : lst1)
         System.out.println(item);
      // Mon
      // Tue
      for (String item : lst2)
         System.out.println(item);
      // Wed
      // Thu

      List<String> lst3 = List.of("Fri", "Sat"); // JDK 9
      System.out.println(lst3);
      // [Fri, Sat]
      System.out.println(Arrays.toString(lst3.toArray()));
      // [Fri, Sat]
      lst3.forEach(System.out::println); // JDK 8
      // Fri
      // Sat
   }
}
(JDK 9) Diamond operator is allowed in anonymous inner classes

From JDK 9, as long as the inferred type is denotable, you can use the diamond operator <> when you create an anonymous inner class.

For example, [TODO]

(JDK 7,9) Simplified varargs Method Declaration with @SafeVarargs Annotation

(JDK 7) New @SafeVarargs

From JDK 7, you have the option of using @SafeVarargs annotation to suppress the warning you get when compiling a method with a non-reifiable varargs parameter. This annotation should be used when the method ensures that only elements of the same type as the varargs parameter are stored in the varargs array.

Example [TODO]

(JDK 9) @SafeVargs annotation is allowed on private instance methods

In JDK 7, the @SafeVarargs annotation can be applied only to methods that cannot be overridden. These include static methods, final instance methods. From JDK 9, @SafeVargs annotation is allowed on private instance methods.

(JDK 8,9) New methods in interface

Prior to JDK 8, only public abstract methods and public static final variables are allowed inside an interface.

(JDK 8) public default method and public static method

JDK 8 enhances interface by introducing these new methods:

  • public default method (JDK 8)
  • public static method (JDK 8)

For examples,

public interface MyJ8Interface {
   // prior to JDK 8
   String NAME = "peter";  // public static final
   void sayHello();        // public abstract

   // JDK 8 public default method - may be overridden
   default void sayHi() {  // public
      System.out.print("(public default method) ");
      System.out.println("hello, " + NAME);
   }

   // JDK 8 public static method - must invoke via interface name 
   static void sayGoodDay() {  // public
      System.out.print("(public static method) ");
      System.out.println("good day, " + NAME);
   }
}
public class MyJ8InterfaceImpl1 implements MyJ8Interface {
   public void sayHello() {
      System.out.print("(public abstract implementation) ");
      System.out.println("hello, world");
   }

   public static void main(String[] args) {
      MyJ8InterfaceImpl1 obj1 = new MyJ8InterfaceImpl1();
      obj1.sayHello();            // run public abstract implementation
      obj1.sayHi();               // run public default
      MyJ8Interface.sayGoodDay();   // run public static via interface name
   }
}
(public abstract implementation) hello, world
(public default method) hello, peter
(public static method) good day, peter
(JDK 9) private method and private static method

JDK 9 introduces these new methods:

  • private (instance) methods
  • private static methods: can be called by other public|private static methods within the same interface.

They are used as helper methods within the interface to remove redundant codes.

See "Enhanced Interface, Lambda Expressions, Streams and Functional Programming" for more details.

(JDK 8) Lambda Expression

JDK 8 introduces lambda expressions, which provides a shorthand notation for creating an instance of an anonymous inner class implementing a functional interface (single-abstract-method interface). The new syntax is as follows:

(arg1, arg2,...) -> abstract-method-body;
   // Return an instance implementing the functional interface

For examples,

[TODO]

This is a major update to support functional programming - started in JDK8 and continue into next few releases. See "Enhanced Interface, Lambda Expressions, Streams and Functional Programming".

(JDK 9) Java Module System

See "Java Module System".

(JDK 9) jshell

JDK 9 introduces a new tool called jshell to support REPL (Read-Evaluate-Print-Loop). It is used to execute and test any Java constructs like class, interface, enum, object, statements etc.

This is NOT a language feature.

(JDK 10,11) var - Type Inference

  • (JDK 10) JEP 286: Local-Variable Type Inference with var
  • (JDK 11) JEP 323: Local-Variable Syntax for Lambda Parameters with var
(JDK 10) Type Inference (var) for Local-Variables with Initializers (JEP 286)

Prior to JDK 10, all local variable declarations required an explicit (manifest) type on the left-hand side. JDK 10 introduces a new keyword var to extend type inference for declarations of local variables with initializers (JEP 286), while maintaining Java's commitment to static type safety.

For examples,


public class J10VarTest {
   public static void main(String[] args) {
      // inferred type from initialized value
      var anInt = 88;
      var aDouble = 55.66;
      var msg = "hello";
      // print type
      System.out.println(msg.getClass().getName());              // for object
      System.out.println(((Object)anInt).getClass().getName());  // for primitive (wrapper)
      System.out.println(((Object)aDouble).getClass().getName());
   }
}
java.lang.String
java.lang.Integer
java.lang.Double

"There is a certain amount of controversy over this feature. Some welcome the concision it enables; others fear that it deprives readers of important type information, impairing readability. And both groups are right. It can make code more readable by eliminating redundant information, and it can also make code less readable by eliding useful information. Another group worries that it will be overused, resulting in more bad Java code being written. This is also true, but it’s also likely to result in more good Java code being written. Like all features, it must be used with judgment. There’s no blanket rule for when it should and shouldn’t be used."

(JDK 11) Type Inference (var) for Local-Variable for Lambda Parameters (JEP 323)

In JDK 11, var is allowed when declaring the formal parameters of implicitly typed lambda expressions.

Read "Local Variable Type Inference Style Guidelines" @ https://openjdk.org/projects/amber/guides/lvti-style-guide.

(JDK 11) Launch Single-File Source-Code Programs (JEP 330)

Single-file programs, where the whole program is kept in a single source file, are common in learning Java.

From JDK 11, you can run a single-file program directly from the source-code with the java launcher, without explicitly compiling the source (via javac).

For example, you can launch the Hello.java directly as follows:

java Hello.java

Notes:

  • This is applicable to single-file source only.
  • No external .class file will be generated.
  • It compiles the source in the memory, and executes directly from the memory.
  • This feature is introduced for beginners to learn Java, and for professionals to test a Java feature.
  • The filename and classname need not be the same.

This is NOT a language feature.

(JDK 13,14,15) Text Blocks

  • (JDK 13) JEP 355: Text Blocks (Preview)
  • (JDK 14) JEP 368: Text Blocks (Second Preview)
  • (JDK 15) JEP 378: Text Blocks (Standard)

Read "Programmer's Guide To Text Blocks" @ https://docs.oracle.com/en/java/javase/14/text-blocks/index.html.

(JDK 13) JEP 355: Text Blocks (Preview)

In earlier releases of the JDK, embedding multi-line code snippets required a tangled mess of explicit new line, string concatenations, and delimiters. JDK 13 proposes to support multi-line string literal (or text block) (JEP 355) that eliminates most of these obstructions, and automatically formats the multi-line string in a predictable way.

A multi-line text block is delimited by a pair of triple double quotes, i.e., """ ... """, which may span over multiple lines. The object produced from a text block is a java.lang.String with the same characteristics as a traditional double quoted string.

For example,

public class J15TextBlock {
   public static void main(String[] args) {
      String htmlStr = """
            <html>
              <head>
                <title>Hello</title>
              </head>
              <body>
                <p>"Hello, world!"</p>
              </body>
            </html>
            """; // A multi-line text block delimited by """......"""
                 // Leading incidental whitespaces and trailing whitespaces are stripped

      System.out.println(htmlStr);  // automatically formatted (see output below)

		// variable htmlStr is the same as:
      htmlStr =
            "<html>\n" +
            "  <head>\n" +
            "    <title>Hello</title>\n" +
            "  </head>\n" +
            "  <body>\n" +
            "    <p>\"Hello, world!\"</p>\n" +     // need escape sequence for "
            "  </body>\n" +
            "</html>\n";
   }
}
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <p>"Hello, world!"</p>
  </body>
</html>

Notes:

  • Text block must begin with """ on the first line (by itself only). The content starts from the next line as in the above example.
    String s1 = """testing""";   // error: no single line text block
    String s2 = """testing 1
                testing 2
                """;   // error: the opening """ must be on its own line
    
    String s3 = """
          testing 1
          testing 2
          """;   // OK
    String s4 = "testing 1\n" +
                "testing 2\n";    // s3 is the same as s4
    System.out.println(s3.equals(s4));  //true
    
    String s5 = """
          testing 1
          testing 2""";   // OK, no trailing newline
    String s6 = "testing 1\n" +
                "testing 2";    // s5 is the same as s6
    System.out.println(s5.equals(s6));  //true
    System.out.println(s5.equals(s4));  //false
  • Text block is automatically formatted by removing incidental leading whitespaces. The entire contents of the text block is shifted to the left until the line with the least leading white space has no leading white space. A text block can opt out of incidental white space stripping by positioning the closing delimiter in the first character position of a source line, e.g.,
    String s7 = """
             testing 1
             testing 2
          """;
    //   testing 1
    //   testing 2
    
    String s8 = """
             testing 1
             testing 2
    """;  // All whitespaces preserved
    //         testing 1
    //         testing 2
    
    String s9 = """
          testing 1
          testing 2""";  // no trailing newline
    //testing 1
    //testing 2
    
  • Trailing whitespaces are stripped.
  • There is no need to use escape sequence for double-quote inside a text block.
  • For most multi-line strings, place the opening delimiter at the right end of the previous line, and place the closing delimiter on its own line, at the left margin of the text block. The contents shall start at 1-2 indents, like the variable html in the first example.
(JDK 14) JEP 368: New Escape Sequences (Preview)

JDK 14 adds two new escape sequences:

  • \<newline>: a backslash (\) at the end of the line suppresses the newline, i.e., continue the next line.
  • \s: to indicate a single space, can be used to create trailing whitespaces.

For example,

String s10 = """
      testing 1 \
      testing 2 \
      """;
//testing 1 testing 2

String s11 = """
      testing 1     \s=
      testing 2     \s=
      """;   // with trailing whitespaces
//testing 1      =
//testing 2      =

(JDK 14,15,16) Pattern Matching for instanceof

  • JEP 305: Pattern Matching for instanceof (Preview)
  • JEP 375: Pattern Matching for instanceof (Second Preview)
  • JEP 394: Pattern Matching for instanceof (Standard)
(JDK 14) JEP 305: Pattern Matching for instanceof (Preview)

JDK 14 has introduced pattern matching for instanceof with the aim of eliminating boilerplate code.

For example, before this feature, we wrote:

if (obj instanceof String) {
    String str = (String) obj;
    int len = str.length();
    // ...
}

This is a very common pattern in Java. Whenever we check if a variable is a certain type, we almost always follow it with a cast to that type.

Now, we can write:

if (obj instanceof String str) {
    int len = str.length();
    // ...
}

In future releases, Java is going to come up with pattern matching for other constructs such as a switch.

JDK 15, 16 [MORE]

(JDK 14,15,16) Record

JDK 14 introduces record (JEP 359) as a preview feature, with a second review in JDK 15 (JEP 384), and standardized in JDK 16 (JEP 395).

(JDK 14) JEP 359: Records (Preview)

A common complaint of Java is there is too much boilerplate codes. Records were introduced to reduce boilerplate code in immutable data model. For example, a data model for a User with an id and name can be simply defined as:

public record User(int id, String name) { }

A new keyword "record" is introduced. This simple declaration will automatically add a canonical constructor with matching parameters, getters, equals(), hashCode() and toString() methods. Record is a restricted form of class, like enum. Records are immutable.


import java.util.Objects;
// Before "record"
public class User {
   private final int id;  // immutable
   private final String name;
   
   public User(int id, String name) {  // constructor
      this.id = id;
      this.name = name;
   }
   public int getId() {  // getters
      return this.id;
   }
   public String getName() {
      return this.name;
   }
   @Override public boolean equals(Object obj) {
      if (!(obj instanceof User)) return false;
      var other = (User)obj;
      return other.id == this.id && other.name.equals(this.name);
   }
   @Override public int hashCode() {
      return Objects.hash(id, name);
   }
}

The above class can be defined in one line with record:

public record User(int id, String name) {}
public class J16RecordUserTest {
   public static void main(String[] args) {
      User user = new User(123, "Tan Ah Teck");  // canonical constructor
      System.out.println(user.id());  // getters
      System.out.println(user.name());
      System.out.println(user);  // toString()
   }
}

You are allow to override the default implementation of the implicitly-defined methods, for example,

public record Student(int id, String name) {
   public Student(int id, String name) {  // canonical constructor
      if (id < 0)
         throw new IllegalArgumentException("id=" + id + " < 0");
      this.id = id;
      this.name = name;
   }

   @Override
   public int id() {
      return 0;
   }

   @Override
   public String toString() {
      return "This is a Student";
   }

   public static void main(String[] args) {  // test driver
      var s1 = new Student(0, "Peter");
      System.out.println(s1);
      //This is a Student
      var s2 = new Student(-9, "Paul");
      //Exception in thread "main" java.lang.IllegalArgumentException: id=-9 < 0
   }
}

You can also add more methods to record, for example,

public record Rectangle (double length, double width) {
   public double area() {
      return length * width;
   }

   public static void main(String[] args) {   // test driver
      var r1 = new Rectangle(2.2, 1.1);
      System.out.println(r1);
      //Rectangle[length=2.2, width=1.1]
      System.out.println(r1.area());
      //2.4200000000000004
   }
}

Take note that records do have some restrictions, e.g., the fields are always final, they cannot be declared abstract, and they cannot use native methods.

Note that the introduction of classes in the java.lang package is rare but necessary from time to time, such as Enum in JDK 5, Module in JDK 9, and Record in JDK 14 (preview).

Constructor for the record class

A record without any constructor is automatically given a canonical constructor that assigns all the private fields to the corresponding arguments of the new expression which instantiated the record. (In contrast, a class without any constructor is automatically given a default constructor that does nothing.)

Compact Canonical Constructor

You can override the constructor in the usual way. There is also a compact way, for example,

public record Time(int hour, int minute, int second) {
   // Compact form to override the canonical constructor to do validation
   public Time {  // The parameters (int hour, int minute, int second) are declared implicitly
      if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59)
         throw new IllegalArgumentException(String.format("%02d:%02d:%02d", hour, minute, second));
      // At the end of the constructor, implicitly run:
      // this.hour = hour; this.minute = minute; this.second = second;
   }

   public static void main(String[] args) {
      var t1 = new Time(23, 59, 59);
      System.out.println(t1);
      //Time[hour=23, minute=59, second=59]
      var t2 = new Time(23, 60, 59);
      //Exception in thread "main" java.lang.IllegalArgumentException: 23:60:59
      System.out.println(t2);
   }
}

JDK 15, 16 [MORE]

(JDK 21,22) String Templates

  • JEP 430: String Templates (Preview)
  • JEP 459: String Templates (Second Preview)
Embedded Expressions \{expr}

String templates complement Java's existing string literals and text blocks by coupling literal text with embedded expressions and template processors to produce specialized results. The embedded expression is in the form of \{expr}.

The STR String Template Processor

For example,

//import static java.lang.StringTemplate.STR;  // not needed, imported automatically

String name = "Peter";
String greeting = STR."Hello \{name}";
System.out.println(greeting);

String title = "My Hello";
String message = "hello, world";
String html = STR."""
      <html>-
        <head>
          <title>\{title}</title>
        </head>
        <body>
          <p>"\{message}"</p>
        </body>
      </html>
      """;
System.out.println(html);

STR (String Template Processor) performs string interpolation by replacing each embedded expression in the template with the (stringified) value of that expression. The result of evaluating a template expression which uses STR is a String.

The embedded expressions are evaluated and the results are converted to String. For example,

int x = 8, y = 9;
String result = STR."\{2*x} + \{3*y} = \{2*x + 3*y}";
System.out.println(result);
//16 + 27 = 43

System.out.println(STR."\{x++}, \{x++}, \{x++}");  // evaluate from left to right
//8, 9, 10

System.out.println(STR."A random number: \{Math.random()}");  // can invoke method

System.out.println(STR."\{x} is \{(x % 2 == 0 ? "even" : "odd")}"); // escape not needed for double-quote
//11 is odd
The FMT Format Template Processor

FMT is another template processor. FMT is like STR in that it performs interpolation, but it also interprets format specifiers which appear to the left of embedded expressions. For example,

import static java.util.FormatProcessor.FMT;

int hour = 23;
int minute = 1;
int second = 5;
String time = FMT."The time is %02d\{hour}:%02d\{minute}:%02d\{second}";
System.out.println(time);
//The time is 23:01:05

String Template is a preview feature in JDK 21. You need to compile and run the program with "--enable-preview -source 21" flags.

JDK 22 [MORE]

Latest version tested: JDK 21.0.1
Last modified: January 2024