Java 17 (September 2021)¶
Release Information
Release Date: September 14, 2021 End of Support: September 2029 Theme: Type Safety & Expressiveness
Overview¶
Java 17 represents a major leap in type safety and code expressiveness. It finalizes several preview features that make Java code more concise while maintaining type safety. This is currently the most popular LTS version for new projects.
Records¶
Interview Favorite
Know that records are immutable, have auto-generated methods, and when to use them vs classes.
The Problem¶
Simple data classes required massive amounts of boilerplate code.
Before/After¶
// 50+ lines for a simple data class
public final class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String name() { return name; }
public int age() { return age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person[name=" + name + ", age=" + age + "]";
}
}
// One line!
public record Person(String name, int age) { }
// Usage is identical
var person = new Person("Alice", 30);
System.out.println(person.name()); // "Alice"
System.out.println(person.age()); // 30
System.out.println(person); // Person[name=Alice, age=30]
// Records can have:
public record Person(String name, int age) {
// Compact constructor for validation
public Person {
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
name = name.trim(); // Can modify before assignment
}
// Additional methods
public String greeting() {
return "Hello, " + name;
}
// Static fields and methods
public static Person unknown() {
return new Person("Unknown", 0);
}
}
Record Characteristics¶
| Feature | Description |
|---|---|
| Immutable | All fields are final |
| Transparent | State is fully described by components |
| Auto-generated | constructor, accessors, equals(), hashCode(), toString() |
| Cannot extend | Implicitly final, cannot extend other classes |
| Can implement | Can implement interfaces |
Sealed Classes¶
The Problem¶
No way to restrict which classes could extend a class/interface.
Before/After¶
// Anyone can extend Shape
public abstract class Shape {
public abstract double area();
}
// In another package, someone creates...
public class MysteryShape extends Shape {
public double area() { return -1; } // Unexpected!
}
// You can't write exhaustive switch
// because you don't know all subclasses
// Only permitted classes can extend
public sealed abstract class Shape
permits Circle, Rectangle, Triangle {
public abstract double area();
}
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
@Override
public double area() { return Math.PI * radius * radius; }
}
public final class Rectangle extends Shape {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() { return width * height; }
}
public non-sealed class Triangle extends Shape {
// non-sealed allows further extension
@Override
public double area() { /* ... */ }
}
Sealed Class Modifiers¶
| Modifier | Meaning |
|---|---|
sealed |
Only permitted subclasses can extend |
final |
Cannot be extended further |
non-sealed |
Opens up for extension (escapes sealing) |
Combined with Pattern Matching¶
// Compiler knows all cases - exhaustive!
double calculateArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> t.base() * t.height() / 2;
// No default needed - compiler verifies exhaustiveness
};
}
Pattern Matching for instanceof¶
Interview Favorite
This eliminates the cast after instanceof check.
The Problem¶
After instanceof check, explicit cast was still required.
Before/After¶
// Redundant cast after instanceof
if (obj instanceof String) {
String s = (String) obj; // Redundant!
System.out.println(s.toUpperCase());
}
// Nested checks even worse
if (obj instanceof List) {
List<?> list = (List<?>) obj;
if (!list.isEmpty()) {
Object first = list.get(0);
if (first instanceof String) {
String s = (String) first;
// Finally use s...
}
}
}
// Pattern variable in same expression
if (obj instanceof String s) {
System.out.println(s.toUpperCase()); // s already cast
}
// With conditions
if (obj instanceof String s && s.length() > 5) {
System.out.println(s.toUpperCase());
}
// In complex conditions
if (obj instanceof List<?> list && !list.isEmpty()
&& list.get(0) instanceof String s) {
System.out.println(s.toUpperCase());
}
// Negation pattern
if (!(obj instanceof String s)) {
return; // s not in scope here
}
// s is in scope here!
System.out.println(s.toUpperCase());
Scope Rules¶
// Variable scope follows flow analysis
if (obj instanceof String s) {
// s in scope
} else {
// s NOT in scope
}
// Works with && but not ||
if (obj instanceof String s && s.length() > 0) { } // OK
// if (obj instanceof String s || s.length() > 0) { } // ERROR
Text Blocks¶
The Problem¶
Multi-line strings were painful with concatenation and escape characters.
Before/After¶
// JSON - nightmare of escapes and concatenation
String json = "{\n" +
" \"name\": \"Alice\",\n" +
" \"age\": 30,\n" +
" \"email\": \"alice@example.com\"\n" +
"}";
// HTML - even worse
String html = "<html>\n" +
" <body>\n" +
" <h1>Hello World</h1>\n" +
" </body>\n" +
"</html>";
// SQL - hard to read and maintain
String sql = "SELECT u.id, u.name, o.total " +
"FROM users u " +
"JOIN orders o ON u.id = o.user_id " +
"WHERE o.status = 'COMPLETED' " +
"ORDER BY o.total DESC";
// JSON - clean and readable
String json = """
{
"name": "Alice",
"age": 30,
"email": "alice@example.com"
}
""";
// HTML - exactly as you'd write it
String html = """
<html>
<body>
<h1>Hello World</h1>
</body>
</html>
""";
// SQL - readable queries
String sql = """
SELECT u.id, u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE o.status = 'COMPLETED'
ORDER BY o.total DESC
""";
// With string formatting
String greeting = """
Hello %s,
Welcome to %s!
""".formatted(name, appName);
Text Block Features¶
| Feature | Example |
|---|---|
| Incidental whitespace | Leading whitespace is trimmed based on closing """ position |
| Line terminators | Always normalized to \n |
| Escape sequences | \", \\, \n still work |
| New escapes | \s (space), \ at end of line (no newline) |
// Control trailing newline
String withNewline = """
text
"""; // Ends with newline
String noNewline = """
text"""; // No trailing newline
// Escape end of line to avoid newline
String singleLine = """
This is a \
single line""";
// Result: "This is a single line"
Switch Expressions¶
Finalized in Java 14
Preview in Java 12-13, standard in Java 14+.
The Problem¶
Switch statements were procedural, error-prone (fall-through), and couldn't return values.
Before/After¶
// Must use variable and break statements
int numLetters;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
numLetters = 6;
break; // Easy to forget!
case TUESDAY:
numLetters = 7;
break;
case THURSDAY:
case SATURDAY:
numLetters = 8;
break;
case WEDNESDAY:
numLetters = 9;
break;
default:
throw new IllegalArgumentException();
}
// Expression that returns a value
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};
// With blocks when logic needed
String description = switch (day) {
case MONDAY -> {
logger.info("Week starting");
yield "Start of work week"; // yield for blocks
}
case FRIDAY -> "End of work week";
case SATURDAY, SUNDAY -> "Weekend!";
default -> "Midweek";
};
// Can still use as statement
switch (command) {
case "start" -> startServer();
case "stop" -> stopServer();
case "restart" -> {
stopServer();
startServer();
}
default -> System.out.println("Unknown command");
}
Switch Expression Rules¶
| Aspect | Arrow Form (->) |
Colon Form (:) |
|---|---|---|
| Fall-through | No | Yes (traditional) |
| Multiple labels | case A, B -> |
case A: case B: |
| Return value | Expression or yield |
Must use yield |
| Block | -> { yield value; } |
{ yield value; } |
Helpful NullPointerException Messages¶
Before/After¶
Quick Reference¶
// Record
public record Point(int x, int y) { }
// Sealed class
public sealed class Shape permits Circle, Square { }
// Pattern matching instanceof
if (obj instanceof String s && s.length() > 0) {
System.out.println(s.toUpperCase());
}
// Text block
String json = """
{"name": "value"}
""";
// Switch expression
int result = switch (x) {
case 1, 2 -> 10;
case 3 -> 20;
default -> 0;
};