1. Modern Java Idioms

Modern Java is still Java: classes, methods, packages, and static typing are the foundation. The difference is that many common patterns now have clearer language support. This chapter focuses on stable features available in the JDK 25 LTS baseline.

1.1. Records for data

Use records for small immutable data carriers. A record declares its state in the header and Java generates the constructor, accessors, equals(), hashCode(), and toString().

 1public record Student(String id, String firstName, String lastName, int grade) {
 2
 3  public String fullName() {
 4    return firstName + " " + lastName;
 5  }
 6
 7  public boolean passing() {
 8    return grade >= 60;
 9  }
10}

Records can have normal methods when the data has behavior closely tied to it. Callers use accessor methods named after the components.

1public static void main(String[] args) {
2  var student = new Student("s-1001", "Ada", "Lovelace", 98);
3
4  System.out.println(student);
5  System.out.println(student.id());
6  System.out.println(student.fullName());
7  System.out.println(student.passing());

Use a class instead of a record when identity, mutation, inheritance, or a long lifecycle is central to the object.

1.2. Sealed hierarchies

Use sealed types when a hierarchy has a known set of implementations. This is common for domain alternatives such as commands, events, messages, or result types.

1sealed interface Media permits Book, Movie {}
2
3record Book(String title, String author, int pages) implements Media {}
4
5record Movie(String title, int minutes) implements Media {}

The compiler can use the sealed hierarchy to check whether a switch handles every possible subtype.

1.3. Pattern matching

Pattern matching removes the old “check, cast, assign” ceremony around instanceof.

1static int length(Object value) {
2  if (value instanceof String text) {
3    return text.length();
4  }
5
6  return 0;
7}

Pattern matching for switch and record patterns work well with sealed hierarchies. The switch below handles every permitted Media subtype and decomposes records directly in the case labels.

 1static String describe(Media media) {
 2  return switch (media) {
 3    case Book(String title, String author, int pages) when pages > 500 ->
 4        "long book: " + title + " by " + author;
 5    case Book(String title, String author, int pages) ->
 6        "book: " + title + " by " + author;
 7    case Movie(String title, int minutes) ->
 8        "movie: " + title + " (" + minutes + " minutes)";
 9  };
10}

This style is usually clearer than a chain of if statements when the input can be one of several known shapes.

1.4. Module imports

JDK 25 supports module import declarations. They import the public API exported by a module and are useful in small programs, examples, and exploratory code.

 1import module java.base;
 2
 3public class ModuleImportDemo {
 4
 5  public static void main(String[] args) {
 6    List<String> names = List.of("Ada", "Grace", "Katherine");
 7    Path report = Path.of("names.txt");
 8
 9    String text = names.stream()
10        .map(String::toUpperCase)
11        .collect(Collectors.joining(", "));
12
13    System.out.println(report.toAbsolutePath());
14    System.out.println(text);

Explicit imports are still better in many production files because they show exactly which external names the file uses. Module imports are strongest when they lower beginner friction or make short examples easier to read.

1.5. Flexible constructor bodies

JDK 25 lets a constructor do validation and simple preparation before calling super(...). This keeps invalid values out of the parent constructor.

1static class PositiveRectangle extends Rectangle {
2
3  PositiveRectangle(int width, int height) {
4    if (width <= 0 || height <= 0) {
5      throw new IllegalArgumentException("dimensions must be positive");
6    }
7
8    super(width, height);
9  }

The code before super(...) is still restricted. It cannot read from the partly constructed object through this.

1.6. Unnamed variables and patterns

Use _ when a variable or pattern component is intentionally unused.

try {
  int value = Integer.parseInt(text);
  System.out.println(value);
} catch (NumberFormatException _) {
  System.out.println("not a number");
}

This avoids fake names such as ignored or unused. It also tells the reader and compiler that the value is deliberately not used.

1.7. Compact source files

For small programs, scripts, and first lessons, compact source files avoid the full class wrapper.

void main() {
  IO.println("Hello, world!");
}

Use compact source files at the beginning of the learning path. Move to normal class-based files when you need packages, tests, multiple classes, or ordinary project structure.