LTS New Features – [ Java 17 ]

Java 17 is the second long-term support (LTS) release after Java 8, it contains the following major new features compared to previous versions :

  • Text Blocks;
  • Improved Switch Statements;
  • Record Type;
  • Sealed Classes;
  • Pattern Matching with instanceof;
  • Helpful NullPointerException;
  • Compact Number Formatting Support;
  • Day Period Support.

1. Text Blocks

Java 17 adds text blocks feature to declare multi-line strings most efficiently.

Text blocks start with three double-quote marks followed by optional whitespaces and a newline.

Inside the text blocks, we can use newlines and quotes without the need for escaping line breaks.

Two new escape characters have been introduced for use inside text blocks, \s for adding a space and \ for removing newline.

In this way, we can include literal fragments of HTML, JSON, SQL etc in a more elegant and readable way.

public class Java17NewFeaturesTest1 {

    public static void main(String[] args) {
        String person = """
                {
                    "name": "tom",
                    "age": 18
                }
                """;

        System.out.println(person);

        String escaped = """
                Text blocks start with a \""" (three double-quote marks) followed by optional whitespaces and a newline.
                """;

        System.out.println(escaped);

        String formated = """
                hi, my name is %s.
                """.formatted("tom");

        System.out.println(formated);

        String sql = """
                select * from user
                where name = 'tom'\sand\sage = 18\
                ;
                """;
        
        System.out.println(sql);
    }
}
/**
        Output:
                {
                    "name": "tom",
                    "age": 18
                }
        
                Text blocks start with a """ (three double-quote marks) followed by optional whitespaces and a newline.
        
                hi, my name is tom.
        
                select * from user
                where name = 'tom' and age = 18;
 */

2. Improved Switch Statements

Switch statement has gone through a rapid evolution in Java 17 :

  • Java 17 has introduced arrow operators as a simple alternative to return a value from a switch expression;
public class Java17NewFeaturesTest2 {

        public static void main(String[] args) {
                System.out.println(getDayOfWeek(1));

                System.out.println(getDayOfWeek(5));

                System.out.println(getDayOfWeek(0));
        }

        private static String getDayOfWeek(int day) {
                String dayOfWeek = switch (day) {
                        case 1 -> "Monday";
                        case 2 -> "Tuesday";
                        case 3 -> "Wednesday";
                        case 4 -> "Thursday";
                        case 5 -> "Friday";
                        case 6 -> "Saturday";
                        case 7 -> "Sunday";
                        default -> "Not a valid day of week !!!";
                };
                return dayOfWeek;
        }
}
/**
        Output:
                Monday
                Friday
                Not a valid day of week !!!
 */
  • 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;
public class Java17NewFeaturesTest3 {

        public static void main(String[] args) {
                System.out.println(getDayOfWeek(1));

                System.out.println(getDayOfWeek(5));

                System.out.println(getDayOfWeek(0));
        }

        private static String getDayOfWeek(int day) {
                String dayOfWeek = switch (day) {
                        case 1 -> {
                                System.out.print("day = 1, it is ");
                                yield "Monday";
                        }
                        case 2 -> {
                                System.out.print("day = 2, it is ");
                                yield "Tuesday";
                        }
                        case 3 -> {
                                System.out.print("day = 3, it is ");
                                yield "Wednesday";
                        }
                        case 4 -> {
                                System.out.print("day = 4, it is ");
                                yield "Thursday";
                        }
                        case 5 -> {
                                System.out.print("day = 5, it is ");
                                yield "Friday";
                        }
                        case 6 -> {
                                System.out.print("day = 6, it is ");
                                yield "Saturday";
                        }
                        case 7 -> {
                                System.out.print("day = 7, it is ");
                                yield "Sunday";
                        }
                        default -> "Not a valid day of week !!!";
                };
                return dayOfWeek;
        }
}
/**
        Output:
                day = 1, it is Monday
                day = 5, it is Friday
                Not a valid day of week !!!
 */
  • Starting Java 17 multiple case values could be provided in a single case statement.
public class Java17NewFeaturesTest4 {

        public static void main(String[] args) {
                System.out.println(getDayOfWeek(1));

                System.out.println(getDayOfWeek(6));

                System.out.println(getDayOfWeek(0));
        }

        private static String getDayOfWeek(int day) {
                String dayOfWeek = switch (day) {
                        case 1, 2, 3, 4, 5 -> "Weekday";
                        case 6, 7 -> "Weekend";
                        default -> "Not a valid day of week !!!";
                };
                return dayOfWeek;
        }
}
/**
        Output:
                Weekday
                Weekend
                Not a valid day of week !!!
 */

3. 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.

Although we can reduce most of our boilerplate code by using libraries like lombok, since Java 17, we can achieve this with the help of records.

public class Java17NewFeaturesTest5 {

        public static void main(String[] args) {
                Person tom = new Person("tom", 18);

                System.out.println(tom);
                System.out.println(tom.name());
                System.out.println(tom.age());
        }

        private static record Person(String name, int age) {
        }
}
/**
 * Output:
        Person[name=tom, age=18]
        tom
        18
 */

A record inherits from the java.lang.Record class.

The compiler will generate an immutable class based on the information provided in the record definition containing :

  • final fields;
  • a constructor to initialize field values;
  • getters;
  • redefining the equals(), hashCode() and toString() methods.
public class Java17NewFeaturesTest5 {

        public static void main(String[] args) {
                Person tom = new Person("tom", 18);

                System.out.println(tom);
                System.out.println(tom.name());
                System.out.println(tom.age());
        }

        private static record Person(String name, int age) {
        }
}
/**
 * Output:
        Person[name=tom, age=18]
        tom
        18

        javap -p Java17NewFeaturesTest5\$Person.class 

        Compiled from "Java17NewFeaturesTest5.java"
        final class Java17NewFeaturesTest5$Person extends java.lang.Record {
                private final java.lang.String name;
                private final int age;
                private Java17NewFeaturesTest5$Person(java.lang.String, int);
                public final java.lang.String toString();
                public final int hashCode();
                public final boolean equals(java.lang.Object);
                public java.lang.String name();
                public int age();
}
 */

It is sometimes necessary to customize the constructor of a record, in particular to validate one or more values ​​provided to initialize the state of the record.

public class Java17NewFeaturesTest8 {

        public static void main(String[] args) {
                try {
                        new Person("", 18);
                } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                }

                System.out.println();

                try {
                        new Person("tom", 0);
                } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                }

                System.out.println();

                Person tom = new Person("tom", 18);

                System.out.println(tom);

        }

        private static record Person(String name, int age) {
                Person(String name, int age) {
                        if (name == null || name.trim().isEmpty()) {
                                throw new IllegalArgumentException("name is required");
                        }

                        if (age <= 0) {
                                throw new IllegalArgumentException("age should >= 1");
                        }

                        this.name = name;
                        this.age = age;
                }
        }
}
/**
 * Output:
        java.lang.IllegalArgumentException: name is required
                at Java17NewFeaturesTest8$Person.<init>(Java17NewFeaturesTest8.java:29)
                at Java17NewFeaturesTest8.main(Java17NewFeaturesTest8.java:5)

        java.lang.IllegalArgumentException: age should >= 1
                at Java17NewFeaturesTest8$Person.<init>(Java17NewFeaturesTest8.java:33)
                at Java17NewFeaturesTest8.main(Java17NewFeaturesTest8.java:13)

        Person[name=tom, age=18]
 */

It is possible to use a shortened syntax to redefine the constructor of a record, this kind of constructor is called compact constructor.

public class Java17NewFeaturesTest9 {

        public static void main(String[] args) {
                try {
                        new Person("", 18);
                } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                }

                System.out.println();

                try {
                        new Person("tom", 0);
                } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                }

                System.out.println();

                Person tom = new Person("tom", 18);

                System.out.println(tom);

        }

        private static record Person(String name, int age) {
                Person {
                        if (name == null || name.trim().isEmpty()) {
                                throw new IllegalArgumentException("name is required");
                        }

                        if (age <= 0) {
                                throw new IllegalArgumentException("age should >= 1");
                        }
                }
        }
}
/**
 * Output:
        java.lang.IllegalArgumentException: name is required
                at Java17NewFeaturesTest9$Person.<init>(Java17NewFeaturesTest9.java:29)
                at Java17NewFeaturesTest9.main(Java17NewFeaturesTest9.java:5)

        java.lang.IllegalArgumentException: age should >= 1
                at Java17NewFeaturesTest9$Person.<init>(Java17NewFeaturesTest9.java:33)
                at Java17NewFeaturesTest9.main(Java17NewFeaturesTest9.java:13)

        Person[name=tom, age=18]
 */

It is possible to redefine an accessor of a record to modify its default behavior.

public class Java17NewFeaturesTest10 {

        public static void main(String[] args) {
                Person tom = new Person("tom", 18);

                System.out.println(tom);
                System.out.println(tom.name);
                System.out.println(tom.name());
        }

        private static record Person(String name, int age) {
                public String name() {
                        return name.toUpperCase();
                }
        }
}
/**
 * Output:
 *       Person[name=tom, age=18]
 *       tom
 *       TOM
 */

A record can be typed with generics.

import java.util.Arrays;
import java.util.List;

public class Java17NewFeaturesTest6 {

        public static void main(String[] args) {
                Person<String> tom = new Person<String>("tom", 18, Arrays.asList("java", "c++"));

                System.out.println(tom);
                System.out.println(tom.name());
                System.out.println(tom.age());
                System.out.println(tom.skills());
        }

        private static record Person<T>(String name, int age, List<T> skills) {
        }
}
/**
 * Output:
 *       Person[name=tom, age=18, skills=[java, c++]]
 *       tom
 *       18
 *       [java, c++]
 */

Records are immutable by default thanks to several characteristics :

  • The record class is final;
  • All declared fields are implicitly final;
  • All declared fields are initialized only by the constructor, there is no setter.

But if the fields are objects, only the references are immutable.

import java.util.ArrayList;
import java.util.List;

public class Java17NewFeaturesTest11 {

        public static void main(String[] args) {
                List<String> skills = new ArrayList<>() {
                        {
                                add("java");
                                add("c++");
                        }
                };

                Person<String> tom = new Person<String>("tom", 18, skills);

                display(tom);

                skills.add("c#");
                tom.skills().add("c");

                display(tom);
        }

        private static void display(Person<String> tom) {
                System.out.println(tom);
                System.out.println(tom.name());
                System.out.println(tom.age());
                System.out.println(tom.skills());
                System.out.println(tom.hashCode());
                System.out.println();
        }

        private static record Person<T>(String name, int age, List<T> skills) {

        }
}
/**
 * Output:
 *       Person[name=tom, age=18, skills=[java, c++]]
 *       tom
 *       18
 *       [java, c++]
 *       211537378
 *
 *       Person[name=tom, age=18, skills=[java, c++, c#, c]]
 *       tom
 *       18
 *       [java, c++, c#, c]
 *       -1615653467
 */

To ensure the complete immutability of a record, returning a copy of the objects is a way to ensure immutability.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class Java17NewFeaturesTest12 {

        public static void main(String[] args) {
                List<String> skills = new ArrayList<>() {
                        {
                                add("java");
                                add("c++");
                        }
                };

                Person<String> tom = new Person<String>("tom", 18, skills);

                display(tom);

                skills.add("c#");
                tom.skills().add("c");

                display(tom);
        }

        private static void display(Person<String> tom) {
                System.out.println(tom);
                System.out.println(tom.name());
                System.out.println(tom.age());
                System.out.println(tom.skills());
                System.out.println(tom.hashCode());
                System.out.println();
        }

        private static record Person<T>(String name, int age, List<T> skills) {
                Person {
                        skills = clone(skills);
                }

                public List<T> skills(){
                        return clone(skills);
                }

                private List<T> clone(List<T> skills) {
                        return skills.stream().collect(Collectors.toList());
                }
        }
}
/**
 * Output:
 *       Person[name=tom, age=18, skills=[java, c++]]
 *       tom
 *       18
 *       [java, c++]
 *       211537378
 *
 *       Person[name=tom, age=18, skills=[java, c++]]
 *       tom
 *       18
 *       [java, c++]
 *       211537378
 */

It is possible to use annotations on a record.

The annotations used on a record should be based on below element type :

  • ElementType.TYPE : for an annotation can be used on a record;
  • ElementType.RECORD_COMPONENT : for an annotation can be used on a component of a record.
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.RecordComponent;

public class Java17NewFeaturesTest7 {

        public static void main(String[] args) throws NoSuchFieldException, SecurityException {
                Person tom = new Person("tom", 18);

                System.out.println(tom);
                System.out.println(tom.name());
                System.out.println(tom.age());

                for (Annotation annotation : tom.getClass().getDeclaredAnnotations()) {
                        System.out.println(annotation);
                }

                for (RecordComponent recordComponents : tom.getClass().getRecordComponents()) {
                        for (Annotation annotation : recordComponents.getDeclaredAnnotations()) {
                                System.out.println(annotation);
                        }
                }
        }

        @MyRecordAnnotation("this is a person")
        private static record Person(@MyRecordComponentAnnotation("this is the name of the person") String name,
                        int age) {
        }

        @Retention(RetentionPolicy.RUNTIME)
        @Target({ ElementType.TYPE })
        public @interface MyRecordAnnotation {
                String value();
        }

        @Retention(RetentionPolicy.RUNTIME)
        @Target({ ElementType.RECORD_COMPONENT })
        public @interface MyRecordComponentAnnotation {
                String value();
        }
}
/**
 * Output:
 *       Person[name=tom, age=18]
 *       tom
 *       18
 *       @Java17NewFeaturesTest7$MyRecordAnnotation(value=this is a person)
 *       @Java17NewFeaturesTest7$MyRecordComponentAnnotation(value=this is the name of the person)
 */

When it comes to the reflection or the introspection on records, two new methods related to records have been added to the java.lang.Class :

  • The isRecord() method returns a boolean that indicates whether the class is a record;
  • The getRecordComponents() method returns an array of type java.lang.reflect.RecordComponent which allows to obtain information about one of the components of a record, including: the name, the type, the annotations, the accessor etc.
import java.lang.reflect.RecordComponent;

public class Java17NewFeaturesTest14 {

        public static void main(String[] args) {
                Person tom = new Person("tom", 18);

                System.out.println(tom);
                System.out.println();

                Class<?> clazz = tom.getClass();

                if (clazz.isRecord()) {
                        RecordComponent[] components = clazz.getRecordComponents();

                        for (int i = 0; i < components.length; i++) {
                                System.out.println(components[i]);
                                System.out.println(components[i].getType());
                                System.out.println(components[i].getName());
                                System.out.println();
                        }
                }
        }

        private static record Person(String name, int age) {
        }
}
/**
 * Output:
 *       Person[name=tom, age=18]
 *
 *       java.lang.String name
 *       class java.lang.String
 *       name
 *
 *       int age
 *       int
 *       age
 *
*/

Records allow us to group a set of values, so it is convenient to declare records to model these values.

It is possible to define a record which will store intermediate values as close as possible to where they will be used.

For example, we can define records in a method.

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class Java17NewFeaturesTest15 {

        public static void main(String[] args) {
                List<ShoppingCart.Item> egg_milk_cheese = new ArrayList<>() {
                        {
                                add(new ShoppingCart.Item("egg", 6));
                                add(new ShoppingCart.Item("milk", 2));
                                add(new ShoppingCart.Item("cheese", 4));
                        }
                };

                List<ShoppingCart.Item> rice_bread_noodle = new ArrayList<>() {
                        {
                                add(new ShoppingCart.Item("rice", 1));
                                add(new ShoppingCart.Item("bread", 2));
                                add(new ShoppingCart.Item("noodle", 1));
                        }
                };

                List<ShoppingCart> carts = new ArrayList<>() {
                        {
                                add(new ShoppingCart(egg_milk_cheese));
                                add(new ShoppingCart(rice_bread_noodle));
                        }
                };

                record Payment(ShoppingCart cart) {
                        int total() {
                                int total = 0;

                                for (ShoppingCart.Item item : cart.items) {
                                        total += item.price;
                                }

                                return total;
                        }
                }
                ;

                carts.stream().map(cart -> new Payment(cart)).sorted((p1, p2) -> p1.total() - p2.total())
                                .map(p -> p.cart.items).forEach(items -> {
                                        List<String> itemsName = items.stream().map(item -> item.name)
                                                        .collect(Collectors.toList());
                                        System.out.println(itemsName);
                                });
        }

        private static class ShoppingCart {
                List<Item> items;

                public ShoppingCart(List<Item> items) {
                        this.items = items;
                }

                private static class Item {
                        String name;
                        int price;

                        public Item(String name, int price) {
                                this.name = name;
                                this.price = price;
                        }
                }
        }
}
/**
 * Output:
 *       [rice, bread, noodle]
 *       [egg, milk, cheese]
 */

4. Sealed Classes

Sealed classes are a feature introduced and finalized in Java 17.

They provide more control over how a class can be extended.

The sealed feature introduces a couple of new modifiers and clauses in Java : sealed, non-sealed, and permits.

To seal a class, we can apply the sealed modifier to its declaration.

The permits clause then specifies the classes that are permitted to extend the sealed class.

public class Java17NewFeaturesTest16 {

        public static void main(String[] args) {
                Shape rectangle = new Rectangle();

                rectangle.display();
        }

        /*
         * private static final class Circle extends Shape {
         * 
         * @Override
         * void display() {
         * System.out.println("I am a circle");
         * }
         * 
         * }
         */
        private static final class Rectangle extends Shape {
                @Override
                void display() {
                        System.out.println("I am a rectangle");
                }

        }

        private static abstract sealed class Shape permits Rectangle {
                abstract void display();
        }
}
/**
 * Output:
 * I am a rectangle
 */

If a class is not permitted to extend the sealed class, an error will be thrown during runtime :

public class Java17NewFeaturesTest16 {

        public static void main(String[] args) {
                Shape rectangle = new Rectangle();

                rectangle.display();

                Shape circle = new Circle();

                circle.display();
        }

        private static final class Circle extends Shape {

                @Override
                void display() {
                        System.out.println("I am a circle");
                }

        }

        private static final class Rectangle extends Shape {
                @Override
                void display() {
                        System.out.println("I am a rectangle");
                }

        }

        private static abstract sealed class Shape permits Rectangle {
                abstract void display();
        }
}
/**
 * Output:
 *       Error: Unable to initialize main class Java17NewFeaturesTest16
 *       Caused by: java.lang.IncompatibleClassChangeError: Failed listed permitted subclass check: class Java17NewFeaturesTest16$Circle is not a permitted subclass of Java17NewFeaturesTest16$Shape
 */

Similar to sealed classes, by applying the sealed modifier to an interface, we can create a sealed interface.

public class Java17NewFeaturesTest17 {
        public static void main(String[] args) {
                Shape rectangle = new Rectangle();

                rectangle.display();
        }

        private static final class Rectangle implements Shape {
                @Override
                public void display() {
                        System.out.println("I am a rectangle");
                }

        }

        private static sealed interface Shape permits Rectangle {
                void display();
        }
}
/**
 * Output:
 * I am a rectangle
 */

A permitted subclass must define a modifier :

  • It may be declared final to prevent any further extensions;
  • It may also be declared sealed to allow certain further extensions;
  • It may also be declared non-sealed to be open for any extensions.
public class Java17NewFeaturesTest18 {
        public static void main(String[] args) {
                Shape rectangle = new Rectangle();

                rectangle.display();

                Shape square = new Square();

                square.display();

                Shape oval = new Oval();

                oval.display();

                Shape circle = new Circle();

                circle.display();
        }

        private static final class Square extends Rectangle {
                @Override
                public void display() {
                        System.out.println("I am a square");
                }
        }

        private static sealed class Rectangle implements Shape permits Square {
                @Override
                public void display() {
                        System.out.println("I am a rectangle");
                }
        }

        private static non-sealed class Oval implements Shape {
                @Override
                public void display() {
                        System.out.println("I am a oval");
                }

        }

        private static class Circle extends Oval {
                @Override
                public void display() {
                        System.out.println("I am a circle");
                }
        }

        private static sealed interface Shape permits Rectangle, Oval {
                abstract void display();
        }
}
/**
 * Output:
 * I am a rectangle
 * I am a square
 * I am a oval
 * I am a circle
 */

Two public methods have been added to the java.lang.Class in order to support Sealed classes:

  • The isSealed() method returns true if the given class or interface is sealed;
  • The getPermittedSubclasses() method returns an array of objects representing all the permitted subclasses.
public class Java17NewFeaturesTest19 {
        public static void main(String[] args) {
                Shape square = new Square();

                square.display();

                Class<?> clazz = square.getClass();

                System.out.println(clazz.isSealed());

                Class<?> superClazz = clazz.getSuperclass();

                System.out.println(superClazz.isSealed());

                Class<?>[] permittedSubclasses = superClazz.getPermittedSubclasses();

                for (int i = 0; i < permittedSubclasses.length; i++) {
                        System.out.println(permittedSubclasses[i]);
                }
        }

        private static final class Square extends Rectangle {
                @Override
                public void display() {
                        System.out.println("I am a square");
                }
        }

        private static sealed class Rectangle implements Shape permits Square {
                @Override
                public void display() {
                        System.out.println("I am a rectangle");
                }
        }

        private static sealed interface Shape permits Rectangle {
                abstract void display();
        }
}
/**
 * Output:
 * I am a square
 * false
 * true
 * class java.features.Java17NewFeaturesTest19$Square
 */

5. Pattern Matching with instanceof

Pattern matching to be used with instanceof operator has been enhanced in Java 17 in order to make it more short and powerful.

Below code snippet shows the traditional way of using instanceof keyword :

import java.util.ArrayList;
import java.util.List;

public class Java17NewFeaturesTest20 {
        public static void main(String[] args) {
                List<Animal> animals = new ArrayList<>() {
                        {
                                add(new Cat());
                                add(new Dog());
                        }
                };

                animals.stream().forEach((Animal animal) -> {
                        if (animal instanceof Cat) {
                                Cat cat = (Cat) animal;
                                cat.moew();
                        }

                        if (animal instanceof Dog) {
                                Dog dog = (Dog) animal;
                                dog.woof();
                        }
                });
        }

        private static interface Animal {

        }

        private static class Cat implements Animal {
                void moew() {
                        System.out.println("I am a cat");
                }
        }

        private static class Dog implements Animal {
                void woof() {
                        System.out.println("I am a dog");
                }
        }
}
/**
 * Output:
 * I am a cat
 * I am a dog
 */

Below code snippet shows the use of Pattern Matching with instanceof keyword :

import java.util.ArrayList;
import java.util.List;

public class Java17NewFeaturesTest21 {
        public static void main(String[] args) {
                List<Animal> animals = new ArrayList<>() {
                        {
                                add(new Cat());
                                add(new Dog());
                        }
                };

                animals.stream().forEach((Animal animal) -> {
                        if (animal instanceof Cat cat) {
                                cat.moew();
                        }

                        if (animal instanceof Dog dog) {
                                dog.woof();
                        }
                });
        }

        private static interface Animal {

        }

        private static class Cat implements Animal {
                void moew() {
                        System.out.println("I am a cat");
                }
        }

        private static class Dog implements Animal {
                void woof() {
                        System.out.println("I am a dog");
                }
        }
}
/**
 * Output:
 * I am a cat
 * I am a dog
 */

6. Helpful NullPointerException

The NPE(NullPointerException) is a runtime exception which is thrown when the code wants to use an object or an object reference that has a null value.

Before Java 17 the common Nullpointer Exception does not indicate where or why the NPE occurred.

It looked something like this in the stack trace :

java.lang.NullPointerException: null

Since Java 17, the NPE points out where and what the null object reference is.

import java.util.HashMap;
import java.util.Map;

public class Java17NewFeaturesTest22 {
        public static void main(String[] args) {
                Map<Integer, Employee> employees = new HashMap<>() {
                        {
                                put(1, new Employee(1, "tom", 18));
                                put(2, new Employee(2, null, 16));
                        }
                };

                System.out.println(employees.get(1).name().toUpperCase());

                try {
                        System.out.println(employees.get(2).name().toUpperCase());
                } catch (Exception e) {
                        e.printStackTrace();
                }

                System.out.println();

                try {
                        System.out.println(employees.get(3).name().toUpperCase());
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }

        private static record Employee(int id, String name, int age) {

        }
}
/**
 * Output:
        TOM
        java.lang.NullPointerException: Cannot invoke "String.toUpperCase()" because the return value of "Java17NewFeaturesTest22$Employee.name()" is null
                at Java17NewFeaturesTest22.main(Java17NewFeaturesTest22.java:16)

        java.lang.NullPointerException: Cannot invoke "Java17NewFeaturesTest22$Employee.name()" because the return value of "java.util.Map.get(Object)" is null
                at Java17NewFeaturesTest22.main(Java17NewFeaturesTest22.java:24)
 */

7. Compact Number Formatting Support

Java 17 introduces compact formatting in order to format numbers in compact human readable form.

We can format long numbers for decimals, currency or percentages into short form or long form.

import java.text.NumberFormat;
import java.util.Locale;

public class Java17NewFeaturesTest23 {
        public static void main(String[] args) {
                int thousand = 1000;
                int million = thousand * thousand;
                int billion = million * thousand;

                System.out.println(thousand);
                System.out.println(million);
                System.out.println(billion);

                System.out.println();

                NumberFormat formatter = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.LONG);

                System.out.println("Long Formats :");
                format(thousand, million, billion, formatter);

                System.out.println();

                formatter = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);

                System.out.println("Short Formats :");
                format(thousand, million, billion, formatter);
        }

        private static void format(int thousand, int million, int billion, NumberFormat formatter) {
                System.out.println(formatter.format(thousand));
                System.out.println(formatter.format(million));
                System.out.println(formatter.format(billion));
        }
}
/**
 * Output:
 * 1000
 * 1000000
 * 1000000000
 *
 * Long Formats :
 * 1 thousand
 * 1 million
 * 1 billion
 *
 * Short Formats :
 * 1K
 * 1M
 * 1B
 */

By default fraction digit is set as zero, but we can set minimum fraction digits as well.

import java.text.NumberFormat;
import java.util.Locale;

public class Java17NewFeaturesTest24 {
        public static void main(String[] args) {
                int fraction = 12;

                int thousand = 1000;
                int thousand_with_fraction = thousand + fraction;

                int million = thousand * thousand;
                int million_with_fraction = million + fraction;

                System.out.println(thousand_with_fraction);
                System.out.println(million_with_fraction);

                System.out.println();

                NumberFormat formatter = NumberFormat.getCompactNumberInstance(Locale.US, NumberFormat.Style.SHORT);

                System.out.println("Without using Fractions :");
                format(thousand_with_fraction, million_with_fraction, formatter);

                System.out.println();

                formatter.setMinimumFractionDigits(2);

                System.out.println("Using Fractions :");

                format(thousand_with_fraction, million_with_fraction, formatter);
        }

        private static void format(int thousand_with_fraction, int million_with_fraction, NumberFormat formatter) {
                System.out.println(formatter.format(thousand_with_fraction));
                System.out.println(formatter.format(million_with_fraction));
        }
}
/**
 * Output:
 * 1012
 * 1000012
 *
 * Without using Fractions :
 * 1K
 * 1M
 *
 * Using Fractions :
 * 1.01K
 * 1.00M
 */

8. Day Period Support

Java 17 adds a new pattern B to DateTime pattern allowing it to specify the time of the day.

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class Java17NewFeaturesTest25 {
        public static void main(String[] args) {
                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
 */