Released September 2021 · Long-term support

Java 17 Features

Java 17 is the long-term-support release that brought modern Java to most teams. It includes records, sealed classes, pattern matching, switch expressions and text blocks, plus a faster and more secure platform. Learn each feature with clear explanations and runnable examples.

  • RecordsData classes in one line
  • Sealed ClassesClosed type hierarchies
  • Pattern Matchinginstanceof without casts
  • Text BlocksReadable multi-line strings
Feature 01

Records

A record (standard since Java 16) is a concise way to declare a class whose job is to hold data. From the component list, the compiler generates private final fields, a constructor, accessor methods, equals(), hashCode() and toString(). A compact constructor lets you add validation without repeating the fields.

  • Syntax: record Point(int x, int y) { }
  • Fields are final, so records are shallowly immutable
  • Accessors are named x() and y(), not getX()
  • Records can implement interfaces and declare methods, but cannot extend a class
RecordDemo.java
import java.util.List;

public class RecordDemo {
    // One line replaces fields, constructor, getters,
    // equals(), hashCode() and toString()
    record Employee(String name, String dept, double salary) {

        // Compact constructor: validate without repeating the fields
        Employee {
            if (salary < 0) {
                throw new IllegalArgumentException("salary < 0");
            }
        }

        // Records can have methods and static factories
        double yearly() {
            return salary * 12;
        }

        static Employee intern(String name) {
            return new Employee(name, "Training", 15000);
        }
    }

    public static void main(String[] args) {
        Employee asha = new Employee("Asha", "IT", 85000);
        Employee same = new Employee("Asha", "IT", 85000);

        System.out.println(asha);
        System.out.println("name()   = " + asha.name());
        System.out.println("yearly() = " + asha.yearly());
        System.out.println("equals   = " + asha.equals(same));
        System.out.println("intern   = " + Employee.intern("Ravi"));

        List<Employee> team = List.of(asha, Employee.intern("Meera"));
        team.forEach(e -> System.out.println(e.name() + " -> " + e.dept()));

        try {
            new Employee("Bad", "IT", -1);
        } catch (IllegalArgumentException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
    }
}
Output
Employee[name=Asha, dept=IT, salary=85000.0]
name()   = Asha
yearly() = 1020000.0
equals   = true
intern   = Employee[name=Ravi, dept=Training, salary=15000.0]
Asha -> IT
Meera -> Training
Rejected: salary < 0
Feature 02

Sealed Classes and Interfaces

Java 17 finalised sealed classes (JEP 409). A sealed class or interface lists its allowed subtypes with permits, so nobody else can extend it. This models closed hierarchies, such as shapes or payment types, and lets the compiler reason about every possible case.

  • Declare with sealed ... permits A, B, C
  • Each subclass must be final, sealed or non-sealed
  • Works very well with records and pattern matching
  • Reflection: isSealed() and getPermittedSubclasses()
SealedDemo.java
public class SealedDemo {
    // Only Circle, Rectangle and Square may implement Shape
    sealed interface Shape permits Circle, Rectangle, Square { }

    // Each permitted subtype must be final, sealed or non-sealed
    record Circle(double radius) implements Shape { }

    record Rectangle(double width, double height) implements Shape { }

    static non-sealed class Square implements Shape {
        final double side;

        Square(double side) {
            this.side = side;
        }
    }

    static double area(Shape shape) {
        if (shape instanceof Circle c) {
            return Math.PI * c.radius() * c.radius();
        } else if (shape instanceof Rectangle r) {
            return r.width() * r.height();
        } else if (shape instanceof Square s) {
            return s.side * s.side;
        }
        throw new AssertionError("unreachable: Shape is sealed");
    }

    public static void main(String[] args) {
        Shape[] shapes = { new Circle(1), new Rectangle(2, 3), new Square(4) };
        for (Shape s : shapes) {
            System.out.printf("%-10s area = %.2f%n",
                    s.getClass().getSimpleName(), area(s));
        }

        System.out.println("Shape is sealed: " + Shape.class.isSealed());
        for (Class<?> c : Shape.class.getPermittedSubclasses()) {
            System.out.println("permits " + c.getSimpleName());
        }
    }
}
Output
Circle     area = 3.14
Rectangle  area = 6.00
Square     area = 16.00
Shape is sealed: true
permits Circle
permits Rectangle
permits Square
Feature 03

Pattern Matching for instanceof

Before Java 16 you had to test a type with instanceof and then cast it. Pattern matching combines both: obj instanceof String s checks the type and, if it matches, binds the value to a new variable s that you can use straight away.

  • No explicit cast, so less code and fewer mistakes
  • The variable can be used later in the same condition: o instanceof Integer i && i > 100
  • Scope follows the flow: after if (!(o instanceof T t)) return;, t is usable
InstanceofPatternDemo.java
public class InstanceofPatternDemo {
    static String describe(Object obj) {
        // Before Java 16: test, then cast
        //   if (obj instanceof String) {
        //       String s = (String) obj; ...
        //   }

        // Java 16+: test and bind a variable in one step
        if (obj instanceof String s) {
            return "String of length " + s.length();
        }
        // The variable can be used in the same condition
        if (obj instanceof Integer i && i > 100) {
            return "Large integer " + i;
        }
        if (obj instanceof Integer i) {
            return "Integer " + i;
        }
        if (!(obj instanceof int[] arr)) {
            return "Something else: " + obj;
        }
        // arr is in scope here because the test above returned
        return "int array with " + arr.length + " items";
    }

    public static void main(String[] args) {
        Object[] values = { "Java 17", 42, 500, new int[] { 1, 2, 3 }, 3.14 };
        for (Object v : values) {
            System.out.println(describe(v));
        }
    }
}
Output
String of length 7
Integer 42
Large integer 500
int array with 3 items
Something else: 3.14
Feature 04

Switch Expressions

Switch expressions became standard in Java 14. Arrow labels (case X ->) never fall through, one case can list several values, and the whole switch can produce a value. For enums, the compiler checks that every constant is covered.

  • case SAT, SUN -> ... handles several values
  • No break and no accidental fall-through
  • yield returns a value from a block
  • Exhaustive: a missing enum constant is a compile error
SwitchDemo.java
public class SwitchDemo {
    enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }

    public static void main(String[] args) {
        // Arrow labels: no fall-through, no break needed
        Day day = Day.SAT;
        switch (day) {
            case SAT, SUN -> System.out.println(day + " is a weekend");
            default -> System.out.println(day + " is a weekday");
        }

        // switch as an expression that returns a value
        for (Day d : new Day[] { Day.MON, Day.WED, Day.SUN }) {
            int hours = switch (d) {
                case MON, TUE, WED, THU -> 8;
                case FRI -> 6;
                case SAT, SUN -> 0;
            };
            System.out.println(d + " -> " + hours + " hours");
        }

        // yield returns a value from a block
        String code = "B";
        String grade = switch (code) {
            case "A" -> "Excellent";
            case "B" -> {
                String msg = "Good";
                yield msg + " (keep going)";
            }
            default -> "Unknown";
        };
        System.out.println("Grade " + code + ": " + grade);
    }
}
Output
SAT is a weekend
MON -> 8 hours
WED -> 8 hours
SUN -> 0 hours
Grade B: Good (keep going)
Feature 05

Text Blocks

A text block (standard since Java 15) starts and ends with three double quotes. It keeps line breaks and quotes as written and removes the common indentation, which makes embedded JSON, SQL, HTML and messages much easier to read.

  • Opening """ must be followed by a line break
  • Incidental indentation is removed automatically
  • \ at the end of a line joins lines; \s keeps a space
  • Use formatted() to fill in values
TextBlockDemo.java
public class TextBlockDemo {
    public static void main(String[] args) {
        // Before: escapes and concatenation
        String oldJson = "{\n" +
                "  \"name\": \"Asha\",\n" +
                "  \"role\": \"Developer\"\n" +
                "}";

        // Java 15+: a text block keeps the layout as written
        String json = """
                {
                  "name": "Asha",
                  "role": "Developer"
                }""";
        System.out.println(json);
        System.out.println("Same text: " + json.equals(oldJson));

        // Combine with formatted() for templates
        String sql = """
                SELECT id, name
                FROM employee
                WHERE dept = '%s'
                """.formatted("IT");
        System.out.print(sql);

        // \ joins lines, \s keeps a trailing space
        String line = """
                Java 17 is a \
                long-term-support release.\s""";
        System.out.println("[" + line + "]");
    }
}
Output
{
  "name": "Asha",
  "role": "Developer"
}
Same text: true
SELECT id, name
FROM employee
WHERE dept = 'IT'
[Java 17 is a long-term-support release. ]
Feature 06

Helpful NullPointerExceptions

In a chain such as order.customer.address.city, an old NullPointerException did not say which reference was null. Since Java 14 (JEP 358), and on by default since Java 15, the JVM describes the failed action and the null expression.

  • Names the field, array or method call that failed
  • Shows the full expression that was null
  • No code changes needed; compile with -g to see local variable names
HelpfulNpeDemo.java
public class HelpfulNpeDemo {
    static class Address {
        String city;
    }

    static class Customer {
        Address address;
    }

    static class Order {
        Customer customer = new Customer();
    }

    public static void main(String[] args) {
        Order order = new Order();
        try {
            // address is null, so which part failed?
            int length = order.customer.address.city.length();
            System.out.println(length);
        } catch (NullPointerException e) {
            // Java 14+ explains exactly what was null
            System.out.println(e.getMessage());
        }

        String[] names = null;
        try {
            System.out.println(names[0]);
        } catch (NullPointerException e) {
            System.out.println(e.getMessage());
        }
    }
}
Output
Cannot read field "city" because "order.customer.address" is null
Cannot load from object array because "names" is null
Feature 07

Enhanced Random Number Generators

Java 17 (JEP 356) added the RandomGenerator interface, implemented by Random, SplittableRandom, ThreadLocalRandom and new LXM algorithms. You can choose an algorithm by name with RandomGeneratorFactory and swap it without changing your code.

  • One interface: java.util.random.RandomGenerator
  • New LXM family, such as L64X128MixRandom
  • Range methods like nextInt(origin, bound) on every generator
  • RandomGenerator.getDefault() picks a good default
RandomDemo.java
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;

public class RandomDemo {
    public static void main(String[] args) {
        // One interface for all random number generators
        RandomGenerator legacy = new java.util.Random(7);
        System.out.println("Random   : " + legacy.nextInt(100));

        // Pick an algorithm by name (seeded so output is repeatable)
        RandomGenerator lxm = RandomGeneratorFactory
                .of("L64X128MixRandom")
                .create(42);
        System.out.println("L64X128  : " + lxm.nextInt(1, 7));

        // New range methods on every generator
        System.out.println("dice     : " + lxm.ints(5, 1, 7)
                .boxed().toList());

        // Default algorithm without choosing one yourself
        RandomGenerator any = RandomGenerator.getDefault();
        int n = any.nextInt(10, 20);
        System.out.println("default  : " + (n >= 10 && n < 20));

        System.out.println("Some algorithms:");
        RandomGeneratorFactory.all()
                .map(RandomGeneratorFactory::name)
                .filter(name -> name.startsWith("L") || name.equals("Random"))
                .sorted()
                .limit(4)
                .forEach(name -> System.out.println("  " + name));
    }
}
Output
Random   : 36
L64X128  : 1
dice     : [2, 1, 1, 4, 4]
default  : true
Some algorithms:
  L128X1024MixRandom
  L128X128MixRandom
  L128X256MixRandom
  L32X64MixRandom
Feature 08

Stream, String and Collector Additions

Releases 12 to 16 added many small methods that you will use every day. Stream.toList() replaces collect(Collectors.toList()), Collectors.teeing merges two collectors, and String gained formatted, indent and transform.

  • Java 12: String.indent, transform, Collectors.teeing, compact number format
  • Java 15: String.formatted, stripIndent
  • Java 16: Stream.toList() and mapMulti
ApiDemo.java
import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class ApiDemo {
    public static void main(String[] args) {
        // Java 16: Stream.toList() returns an unmodifiable list
        List<String> langs = Stream.of("java", "kotlin", "scala")
                .map(String::toUpperCase)
                .toList();
        System.out.println("toList()    : " + langs);

        // Java 15: formatted() is an instance version of String.format
        System.out.println("formatted() : " + "%s has %d items"
                .formatted("cart", 3));

        // Java 12: transform() and indent()
        int words = "Java 17 is here".transform(s -> s.split(" ").length);
        System.out.println("transform() : " + words + " words");
        System.out.print("indent(4)   :\n" + "line one\nline two".indent(4));

        // Java 12: Collectors.teeing() combines two collectors
        double avg = Stream.of(10, 20, 30, 40)
                .collect(Collectors.teeing(
                        Collectors.summingInt(i -> i),
                        Collectors.counting(),
                        (sum, count) -> (double) sum / count));
        System.out.println("teeing()    : average = " + avg);

        // Java 12: compact number formatting
        NumberFormat fmt = NumberFormat.getCompactNumberInstance(
                Locale.US, NumberFormat.Style.SHORT);
        fmt.setMaximumFractionDigits(1);
        System.out.println("compact     : " + fmt.format(1_500)
                + ", " + fmt.format(2_300_000));

        // Java 16: mapMulti() emits zero or more values per element
        List<Integer> evensTwice = Stream.of(1, 2, 3, 4)
                .<Integer>mapMulti((n, out) -> {
                    if (n % 2 == 0) {
                        out.accept(n);
                        out.accept(n * 10);
                    }
                })
                .toList();
        System.out.println("mapMulti()  : " + evensTwice);
    }
}
Output
toList()    : [JAVA, KOTLIN, SCALA]
formatted() : cart has 3 items
transform() : 4 words
indent(4)   :
    line one
    line two
teeing()    : average = 25.0
compact     : 1.5K, 2.3M
mapMulti()  : [2, 20, 4, 40]
Feature 09

JVM and Platform Changes

Java 17 also improved the platform. ZGC and Shenandoah became production-ready, JDK internals are now strongly encapsulated, and Flight Recorder can stream events. Some old features were deprecated or removed, so check these points when upgrading from Java 11.

  • ZGC and Shenandoah: low-pause garbage collectors for large heaps
  • Internal APIs such as sun.misc packages are closed by default
  • Security Manager and Applet API deprecated for removal
  • Native support for macOS on Apple Silicon
Read the full tutorial: Garbage Collection in Java
Commands and notes
# Garbage collectors: ZGC and Shenandoah are production-ready (Java 15)
java -XX:+UseZGC App
java -XX:+UseShenandoahGC App

# JDK internals are strongly encapsulated (JEP 403). Code that uses
# reflection on internal APIs now fails unless you open the package:
java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar

# Inspect a running application with Flight Recorder streaming
jcmd <pid> JFR.start name=live

# Deprecated for removal: Security Manager, Applet API
# Removed: RMI Activation, experimental AOT and Graal JIT compilers
# New ports: macOS on Apple Silicon (AArch64)
What Came Next

Beyond Java 17

Later releases built on these features. Here are the highlights up to the next long-term-support releases:

VersionKey additions
Java 18UTF-8 by default, simple web server (jwebserver)
Java 19–20Virtual threads, record patterns and switch patterns as previews
Java 21 (LTS)Virtual threads, record patterns, pattern matching for switch, sequenced collections
Java 22Unnamed variables (_), Foreign Function & Memory API
Java 23–24Markdown in Javadoc, stream gatherers, class-file API
Java 25 (LTS)Compact source files and instance main, flexible constructor bodies