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
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
GitHub Project
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.
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
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
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
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
GitHub Project
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

Onur Baskirt is a Software Engineering Leader with international experience in world-class companies. Now, he is a Software Engineering Lead at Emirates Airlines in Dubai.