⚡IntermediateModern Java

Records

Define immutable data-carrier classes concisely with the record keyword (Java 16+).

Overview

A record is a special class designed to hold immutable data. Instead of writing a class with private fields, a constructor, getters, equals, hashCode, and toString, you declare a record in one line. record Point(int x, int y) {} automatically gives you a constructor, accessors (x(), y()), equals(), hashCode(), and toString(). Records are immutable by design: all fields are final. They are ideal for data transfer objects (DTOs), configuration values, coordinates, and any data that should not change after creation.

Key Points

  • •record Name(Type field1, Type field2) { } declares a record
  • •Fields (components) are automatically final and private
  • •Accessors are generated: field() method for each component (not getField())
  • •equals() and hashCode() are based on all components
  • •toString() prints ClassName[field1=value1, field2=value2]
  • •You can add custom methods, static methods, and static fields
  • •You can define a compact constructor to validate or normalize inputs
  • •Records cannot extend other classes (implicitly extend java.lang.Record)
  • •Records can implement interfaces

Examples

Basic record usage

public class Main {
    record Point(int x, int y) {}

    record Person(String name, int age) {
        // Compact constructor: no parameter list, runs before field assignment
        Person {
            if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
            name = name.trim(); // normalize; assigned to field automatically
        }

        // Custom method
        String greeting() {
            return "Hello, I am " + name + " and I am " + age + " years old.";
        }
    }

    public static void main(String[] args) {
        var p = new Point(3, 4);
        System.out.println(p);           // Point[x=3, y=4]
        System.out.println(p.x());       // 3  (accessor, not getX())
        System.out.println(p.y());       // 4

        var p2 = new Point(3, 4);
        System.out.println(p.equals(p2)); // true (value-based equality)

        var alice = new Person("  Alice  ", 30);
        System.out.println(alice);           // Person[name=Alice, age=30]
        System.out.println(alice.greeting());
    }
}

The compact constructor does not list parameters; they are in scope automatically. Modifications to parameters (like name.trim()) are applied to the fields before they are set.

Records as DTOs and in collections

import java.util.*;
import java.util.stream.*;

public class Main {
    record Product(String name, String category, double price) {}

    public static void main(String[] args) {
        var products = List.of(
            new Product("Apple", "Fruit", 1.20),
            new Product("Banana", "Fruit", 0.50),
            new Product("Carrot", "Vegetable", 0.80),
            new Product("Daikon", "Vegetable", 1.50)
        );

        // Records work well in streams
        var byCategory = products.stream()
            .collect(Collectors.groupingBy(Product::category));

        byCategory.forEach((cat, ps) -> {
            double avg = ps.stream().mapToDouble(Product::price).average().orElse(0);
            System.out.printf("%s: avg price $%.2f%n", cat, avg);
        });

        // Sort by price
        products.stream()
                .sorted(Comparator.comparingDouble(Product::price))
                .forEach(System.out::println);
    }
}

Product::name, Product::price are method references to the auto-generated accessors. Records integrate naturally with streams, collectors, and comparators.

Records implementing interfaces and with static members

public class Main {
    interface Describable { String describe(); }

    record Circle(double radius) implements Describable {
        static final double PI = Math.PI;

        double area() { return PI * radius * radius; }
        double circumference() { return 2 * PI * radius; }

        @Override
        public String describe() {
            return String.format("Circle(r=%.2f, area=%.2f)", radius, area());
        }
    }

    record Rectangle(double width, double height) implements Describable {
        double area() { return width * height; }

        @Override
        public String describe() {
            return String.format("Rectangle(%sx%s, area=%.2f)", width, height, area());
        }
    }

    public static void main(String[] args) {
        Describable[] shapes = {
            new Circle(5.0),
            new Rectangle(3.0, 4.0),
            new Circle(2.5)
        };
        for (var s : shapes) {
            System.out.println(s.describe());
        }
    }
}

Records can implement interfaces and have instance methods and static members. They just cannot extend other classes or have mutable fields.

Common Mistakes

  • Using getX() style accessors. Record accessors are x(), not getX(). This is a deliberate design decision to distinguish records from regular JavaBeans
  • Trying to modify a record field after construction. All fields are final. Create a new record with the changed values instead
  • Forgetting that records are not replacements for all classes. Records are for immutable data. If you need mutable state, a regular class is still appropriate

Note

Use records for any simple data container: coordinates, configuration, API responses, event objects, and arguments to methods. They eliminate ~80% of the boilerplate code in typical Java data classes.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 4 exercises

Knowledge Check

Question 1 of 4

How do you access the field x of a record Point(int x, int y)?