Java 17 Language Features with Examples

In this article, we will learn the Java 17 language features which we can use in our projects. These are Pattern Matching, Records, Sealed Classes, Switch Expressions, Text Blocks. Some of these features were announced in previous JDK versions as a preview or finalized. In JDK 17 we will have all of these features. Let’s start to learn them one by one.

Pattern Matching – instanceOf Feature

The Pattern matching instanceof feature performs casts after type comparisons. In the example below, condition ‘o instanceof String str‘ is always ‘true‘ for the first and second if statements but at the third one we will get RuntimeException.

public class InstanceOfPatternMatching {
    /**
     * Pattern matching for instanceof performing casts after type comparisons.
     */
    @Test
    public void instanceOfPatternMatchingTest(){
        Object o = "I am a string as an object";
        if (o instanceof String str) {
            System.out.println(str.toUpperCase());
        }

        //The following code is also valid:
        if (o instanceof String str && !str.isEmpty()) {
            System.out.println(str.toUpperCase());
        }

        Object obj = 123;

        //The following code is also valid:
        if (!(obj instanceof String str)) {
            throw new RuntimeException("Please provide string!");
        }
    }
}

Output

instanceof in java

GitHub Project

InstanceOf PatternMatching Example in Java

Records in Java

Records are very useful for the immutable data carrier classes. Its all details and history are explained on this page. Here we can summarize their major features as below: 

  • They are immutable.
  • Their fields are private and final.
  • Record Class defines canonical constructors for all fields.
  • All fields have Getters, equals, hashCode, and toString.

Let’s declare a Record as Footballer below:

record Footballer(String name, int age, String team) { }

With the record declaration above, we automatically define:

  • Private final fields for age, name, and team.
  • Canonical constructors for all fields.
  • Getters for all fields.
  • equals, hashCode, and toString for all fields.

We can also do the following for the records;

  • We can define additional methods.
  • Implement interfaces.
  • Customize the canonical constructor and accessors.

Let’s do a whole example to see it in action.

/**
 * Records reduce boilerplate code for classes that are simple data carriers.
 * They are immutable (since their fields are private and final).
 * They are implicitly final.
 * We cannot define additional instance fields.
 * They always extend the Record class.
 */
public class Records {
    @BeforeEach
    void setup(TestInfo testInfo) {
        System.out.println(testInfo.getDisplayName());
    }

    @AfterEach
    void teardown() {
        System.out.println();
    }

    /**
     * With below record declaration, we automatically define:
     * Private final fields for age, name, and team.
     * Canonical constructors for all fields.
     * Getters for all fields.
     * equals, hashCode, and toString for all fields.
     */
    record Footballer(String name, int age, String team) { }

    //Canonical Constructor
    Footballer footballer = new Footballer("Ronaldo", 36, "Manchester United");

    @Test
    public void recordTest() {
        //Getters without get prefix
        System.out.println("Footballer's name: " + footballer.name);
        System.out.println("Footballer's age: " + footballer.age);

        record Basketballer(String name, int age) { }

        // equals
        boolean isFootballer1 = footballer.equals(new Footballer("Ozil", 32, "Fenerbahce")); // false
        System.out.println("Is first one footballer? " + isFootballer1);

        boolean isFootballer2 = footballer.equals(new Basketballer("Lebron", 36)); // false
        System.out.println("Is second one footballer? " + isFootballer2);

        boolean isFootballer3 = footballer.equals(new Footballer("Ronaldo", 36, "Manchester United")); // true
        System.out.println("Is third one footballer? " + isFootballer3);

        //hashcode
        int hashCode = footballer.hashCode(); // depends on values of x and y
        System.out.println("Hash Code of Record: " + hashCode);

        //toString
        String toStringOfRecord = footballer.toString();
        System.out.println("ToString of Record: " + toStringOfRecord);
    }

    /**
     * We can define additional methods.
     * Implement interfaces.
     * Customize the canonical constructor and accessors.
     */
    @Test
    public void record2Test() {
        record Engineer(String name, int age) {
            //Explicit canonical constructor
            Engineer {
                //Custom validation
                if (age < 1)
                    throw new IllegalArgumentException("Age less than 1 is not allowed!");
                //Custom modifications
                name = name.toUpperCase();
            }

            //Explicit accessor
            public int age() {
                return this.age;
            }
        }

        Engineer engineer1 = new Engineer("Onur", 39);
        System.out.println(engineer1);
        Assertions.assertEquals("ONUR", engineer1.name);

        Exception exception = Assertions.assertThrows(IllegalArgumentException.class, () -> new Engineer("Alex", 0));
        Assertions.assertEquals("Age less than 1 is not allowed!", exception.getMessage());
    }

}

Output

records in java

GitHub Project

Records example in Java

Sealed Classes

The sealed classes permit their subclasses to extend them. The other classes cannot extend the parent if they are not permitted for an extension by the parent class. The sealed interfaces permit the subinterfaces and implementing classes. 

All permitted classes or interfaces in the “permits list” must be declared as final and they needed to be located in the same package. Let’s do an example.

Sealed Class (Shape Class)

Shape class in the parent class for the shapes and it permits extension for Square and Rectangle classes. The other classes cannot extent shape class.

/**
 * Sealed Parent Class which only allows Square and Rectangle as its children.
 */
@Getter
public sealed class Shape permits Square, Rectangle{
    protected int edge1, edge2;

    protected Shape(int edge1, int edge2) {
        this.edge1 = edge1;
        this.edge2 = edge2;
    }
}

Sealed Interface (ShapeService Interface)

ShapeService is the interface and it also permits Square and Rectangle classes to override the methods inside the interface.

/**
 * Sealed Interface
 */
public sealed interface ShapeService permits Square, Rectangle {
    default int getArea(int a, int b) {
        return a * b;
    }

    int getPerimeter();
}

Permitted Class 1 (Rectangle Class)

Rectangle class is the permitted class and it extends the Shape class and implements the ShapeService interface. It must be declared as final. It also overrides the perimeter calculation by getPerimeter() method based on its shape and for the area calculation, it uses the default getArea() method inside the ShapeService interface.

public final class Rectangle extends Shape implements ShapeService {

    public Rectangle(int edge1, int edge2) {
        super(edge1, edge2);
    }

    @Override
    public int getPerimeter() {
        return 2 * (edge1 + edge2);
    }
}

Permitted Class 2 (Square Class)

Square class is another permitted class and it extends the Shape class and implements the ShapeService interface. It must be declared as final. It overrides the perimeter calculation by getPerimeter() method based on its shape and for the area calculation, it uses the default getArea() method inside the ShapeService interface.

public final class Square extends Shape implements ShapeService {
    public Square(int edge1, int edge2) {
        super(edge1, edge2);
    }

    @Override
    public int getPerimeter() {
        return 4 * edge1;
    }
}

UnPermitted Class (Triangle Class)

Triangle class is an unpermitted class and it implements its own features without parent class extension and an interface implementation.

//public final class Triangle extends Shape implements ShapeService {} -> Triangle is not allowed in the sealed hierarchy!!!
public final class Triangle {
    private final int base;
    private final int edge1;
    private final int edge2;
    private final int height;

    public Triangle(int base, int edge1, int edge2, int height) {
        this.base = base;
        this.edge1 = edge1;
        this.edge2 = edge2;
        this.height = height;
    }

    public int getPerimeter() {
        return base + edge1 + edge2;
    }

    public int getArea() {
        return (base * height) / 2;
    }
}

Test Class (ShapeTest Class)

In the below test class, we declared the subclasses and tested their behavior.

public class ShapeTest {
    @Test
    public void shapeTest() {
        /**
         * Permitted classes RECTANGLE and SQUARE
         */
        //Rectangle Declaration and tests
        Rectangle rectangle = new Rectangle(3, 5);

        assertEquals(16, rectangle.getPerimeter());
        assertEquals(15, rectangle.getArea(3, 5));

        //Square Declaration and tests
        Square square = new Square(3, 3);

        assertEquals(12, square.getPerimeter());
        assertEquals(9, square.getArea(3, 3));

        /**
         * Unpermitted Class TRIANGLE
         */
        Triangle triangle = new Triangle(6, 5, 5, 4);

        assertEquals(16, triangle.getPerimeter());
        assertEquals(12, triangle.getArea());
    }
}

Output

All tests passed.

sealed classes in java

GitHub Project

Sealed Class Example in Java 17

Switch Expressions

Switch Expressions are now more concise than before. In order to see the differences, let’s first implement a switch expression in a traditional way, and then we will implement the same logic with the new way.

In the example below, we have positionMap which contains the position number and the football position. The code randomly generates a number from 1 to 5 and in the old switch test, we are printing the randomly selected football positions and footballers. 

public enum Position {
    GOALKEEPER,
    DEFENCE,
    MIDFIELDER,
    STRIKER,
    BENCH
}
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class SwitchExpression {
    private Map<Integer, Position> positionMap = new HashMap<>();
    private int                    randomNumber;
    private Position               randomPosition;

    @BeforeEach
    public void setup() {
        positionMap.put(1, GOALKEEPER);
        positionMap.put(2, DEFENCE);
        positionMap.put(3, MIDFIELDER);
        positionMap.put(4, STRIKER);
        randomNumber = ThreadLocalRandom.current().nextInt(1, 6);
        randomPosition = Optional.ofNullable(positionMap.get(randomNumber)).orElse(BENCH);
    }

    @AfterEach
    public void tearDown() {
        positionMap.clear();
    }

    @RepeatedTest(5)
    @Order(1)
    public void oldSwitchExpressionTest() {
        switch (randomPosition) {
            case GOALKEEPER:
                System.out.println("Goal Keeper: Buffon");
                break;
            case DEFENCE:
                System.out.println("Defence: Ramos");
                break;
            case MIDFIELDER:
                System.out.println("Midfielder: Messi");
                break;
            case STRIKER:
                System.out.println("Striker: Zlatan");
                break;
            default:
                System.out.println("Please select a footballer from the BENCH!");
        }
    }
}

Output

new switch expression in java 17

Now, we can implement the same logic with the new switch statement in Java. Compared to a traditional switch, the new switch expression:

  • Uses “->” instead of “:”
  • Allows multiple constants per case.
  • Does not have fall-through semantics (i.e., Does not require breaks).
  • Makes variables defined inside a case branch local to this branch.
  • A “default” branch has to be provided.
/**
 * Compared to a traditional switch, the new switch expression
 * Uses “->” instead of “:”
 * Allows multiple constants per case.
 * Does not have fall-through semantics (i.e., Does not require breaks).
 * Makes variables defined inside a case branch local to this branch.
 * A “default” branch has to be provided.
 */
@RepeatedTest(5)
@Order(2)
public void newSwitchExpressionTest() {
    switch (randomPosition) {
        case GOALKEEPER -> System.out.println("Goal Keeper: Buffon");
        case DEFENCE -> System.out.println("Defence: Ramos");
        case MIDFIELDER -> System.out.println("Midfielder: Messi");
        case STRIKER -> System.out.println("Striker: Zlatan");
        default -> System.out.println("Please select a footballer from the BENCH!");
    }
}

If the right-hand side of a single case requires more code, it can be written inside a block, and the value is returned using yield.

/**
 * If the right-hand side of a single case requires more code, it can be written inside a block, and the value is returned using yield.
 */
@RepeatedTest(5)
@Order(3)
public void newSwitchExpressionWithAssignmentTest() {
    String footballer = switch (randomPosition) {
        case GOALKEEPER, DEFENCE -> {
            System.out.println("Defensive Footballer Selection!");
            yield "Defence: Ramos";
        }
        case MIDFIELDER, STRIKER -> {
            System.out.println("Offensive Footballer Selection!");
            yield "Midfielder: Messi";
        }
        default -> "Please select a footballer from the BENCH!";
    };
    System.out.println(footballer);
}

GitHub Project

New Switch Expression Implementation in Java

Text Blocks

Let’s start with the Text blocks definition. Text Blocks are blocks of String which can be declared by starting with three double quotes “”” which can be followed by a line break and closed by three double quotes again.

We can use newlines and quotes inside the text blocks without taking care of escape line break characters and in this way it will be much easier and readable to work with JSON, SQL, and similar texts with text blocks.

/**
 * A text block can be declared by starting with three double quotes """ which should be followed by a line break and 
 * closed by three double quotes again.
 */
@Test
public void textBlocksTest() {
    String textBlockFootballers = """
        Footballers
          with double space indentation
            and "SW TEST ACADEMY TEAM" Rocks!
        """;
    System.out.println(textBlockFootballers);
}

Output

text blocks in java

We can make the same text one-liner with the “\” character. Let’s see the example below.

/**
 * We can make the same text one-liner with the "\" character. Let's see the example below.
 */
@Test
public void textBlocksNoLineBreaksTest() {
    String textBlockFootballers = """
        Footballers \
        with double space indentation \
        and "SW TEST ACADEMY TEAM" Rocks! \
        """;
    System.out.println(textBlockFootballers);
}

Output

text blocks in java 17

We can insert variables into a text block by using the static method String::format or with the String::formatted. Let’s check the example below.

/**
 * We can insert variables into a text block by using the static method String::format or with the String::formatted.
 */
@Test
public void textBlocksInsertingVariablesTest() {
    String textBlockFootballers = """
        Footballers
          with double space indentation
            and "%s" Rocks!
        """.formatted("SW TEST ACADEMY TEAM");
    System.out.println(textBlockFootballers);
}

Output

textblocks java17

GitHub Project

TextBlocks Example in Java

In this article, I have shared the new language features from Java 12 to Java 17. I hope you enjoyed reading.

See you in another article,
Onur Baskirt

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.