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
All Java 17 Features at a Glance
Everything that became standard between Java 12 and Java 17, which is what you gain when upgrading from Java 11.
Records
Immutable data classes in a single line.
Sealed Classes and Interfaces
Control exactly which classes may extend a type.
Pattern Matching for instanceof
Test and cast in one step.
Switch Expressions
Arrow labels, multiple values and switch that returns a value.
Text Blocks
Multi-line strings without escapes.
Helpful NullPointerExceptions
Error messages that say exactly what was null.
Enhanced Random Number Generators
A common interface and new algorithms.
Stream, String and Collector Additions
toList, formatted, teeing, mapMulti and more.
JVM and Platform Changes
Production-ready low-pause GCs and stronger encapsulation.
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()andy(), notgetX() - Records can implement interfaces and declare methods, but cannot extend a class
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());
}
}
}
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
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,sealedornon-sealed - Works very well with records and pattern matching
- Reflection:
isSealed()andgetPermittedSubclasses()
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());
}
}
}
Circle area = 3.14 Rectangle area = 6.00 Square area = 16.00 Shape is sealed: true permits Circle permits Rectangle permits Square
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;,tis usable
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));
}
}
}
String of length 7 Integer 42 Large integer 500 int array with 3 items Something else: 3.14
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
breakand no accidental fall-through yieldreturns a value from a block- Exhaustive: a missing enum constant is a compile error
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);
}
}
SAT is a weekend MON -> 8 hours WED -> 8 hours SUN -> 0 hours Grade B: Good (keep going)
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;\skeeps a space- Use
formatted()to fill in values
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 + "]");
}
}
{
"name": "Asha",
"role": "Developer"
}
Same text: true
SELECT id, name
FROM employee
WHERE dept = 'IT'
[Java 17 is a long-term-support release. ]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
-gto see local variable names
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());
}
}
}
Cannot read field "city" because "order.customer.address" is null Cannot load from object array because "names" is null
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
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));
}
}
Random : 36 L64X128 : 1 dice : [2, 1, 1, 4, 4] default : true Some algorithms: L128X1024MixRandom L128X128MixRandom L128X256MixRandom L32X64MixRandom
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()andmapMulti
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);
}
}
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]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.miscpackages are closed by default - Security Manager and Applet API deprecated for removal
- Native support for macOS on Apple Silicon
# 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)
Beyond Java 17
Later releases built on these features. Here are the highlights up to the next long-term-support releases:
| Version | Key additions |
|---|---|
| Java 18 | UTF-8 by default, simple web server (jwebserver) |
| Java 19–20 | Virtual threads, record patterns and switch patterns as previews |
| Java 21 (LTS) | Virtual threads, record patterns, pattern matching for switch, sequenced collections |
| Java 22 | Unnamed variables (_), Foreign Function & Memory API |
| Java 23–24 | Markdown in Javadoc, stream gatherers, class-file API |
| Java 25 (LTS) | Compact source files and instance main, flexible constructor bodies |