DEV Community

Cover image for Java Records Ultimate Guide: How to Write Clean, Immutable Data Classes (2024)
Aarav Joshi
Aarav Joshi

Posted on

Java Records Ultimate Guide: How to Write Clean, Immutable Data Classes (2024)

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Java Record Types transform how we handle data classes in Java, offering a concise way to create immutable objects. As a developer who has implemented records across multiple projects, I've found they significantly reduce boilerplate code while improving code clarity.

Records appeared as a preview feature in Java 14 and became permanent in Java 16. They automatically generate methods like equals(), hashCode(), and toString(), along with a constructor and accessor methods for all fields.

Let's start with a basic record:

public record Person(String name, int age) { }
Enter fullscreen mode Exit fullscreen mode

This simple declaration creates an immutable class with two fields. Behind the scenes, Java generates several methods:

// Automatically generated
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; }

    // equals(), hashCode(), toString() methods
}
Enter fullscreen mode Exit fullscreen mode

Records excel in data transfer scenarios. Here's a practical example using records with Spring Boot:

@RestController
public class UserController {
    record UserResponse(String name, String email, List<String> roles) { }

    @GetMapping("/user/{id}")
    public UserResponse getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return new UserResponse(
            user.getName(),
            user.getEmail(),
            user.getRoles().stream()
                .map(Role::getName)
                .collect(Collectors.toList())
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Records support custom constructors, allowing validation:

public record Employee(String id, String name, double salary) {
    public Employee {
        if (salary < 0) {
            throw new IllegalArgumentException("Salary cannot be negative");
        }
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We can add additional methods to records:

public record Rectangle(double length, double width) {
    public double area() {
        return length * width;
    }

    public boolean isSquare() {
        return length == width;
    }
}
Enter fullscreen mode Exit fullscreen mode

Records work well with Java Stream API:

List<Rectangle> rectangles = List.of(
    new Rectangle(2, 3),
    new Rectangle(4, 4),
    new Rectangle(5, 2)
);

double totalArea = rectangles.stream()
    .map(Rectangle::area)
    .reduce(0.0, Double::sum);

List<Rectangle> squares = rectangles.stream()
    .filter(Rectangle::isSquare)
    .collect(Collectors.toList());
Enter fullscreen mode Exit fullscreen mode

Records can implement interfaces:

public interface Measurable {
    double measure();
}

public record Circle(double radius) implements Measurable {
    @Override
    public double measure() {
        return Math.PI * radius * radius;
    }
}
Enter fullscreen mode Exit fullscreen mode

They're particularly useful in pattern matching:

public double calculateArea(Object shape) {
    return switch (shape) {
        case Rectangle r -> r.length() * r.width();
        case Circle c -> Math.PI * c.radius() * c.radius();
        default -> throw new IllegalArgumentException("Unknown shape");
    };
}
Enter fullscreen mode Exit fullscreen mode

Records support generic types:

public record Pair<T, U>(T first, U second) {
    public <V> Pair<V, U> mapFirst(Function<T, V> mapper) {
        return new Pair<>(mapper.apply(first), second);
    }
}
Enter fullscreen mode Exit fullscreen mode

When working with JPA, records can serve as DTOs:

@Entity
public class Customer {
    @Id
    private Long id;
    private String name;
    private String email;

    public record CustomerDTO(Long id, String name, String email) {
        public static CustomerDTO from(Customer customer) {
            return new CustomerDTO(
                customer.getId(),
                customer.getName(),
                customer.getEmail()
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Records enhance testing clarity:

public class CalculatorTest {
    record TestCase(int a, int b, int expected) { }

    @ParameterizedTest
    @MethodSource("additionCases")
    void testAddition(TestCase testCase) {
        Calculator calc = new Calculator();
        assertEquals(
            testCase.expected(), 
            calc.add(testCase.a(), testCase.b())
        );
    }

    static Stream<TestCase> additionCases() {
        return Stream.of(
            new TestCase(1, 1, 2),
            new TestCase(0, 5, 5),
            new TestCase(-1, 1, 0)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Records support nested definitions:

public record Department(String name, List<Employee> employees) {
    public record Employee(String name, String role) { }

    public List<Employee> findByRole(String role) {
        return employees.stream()
            .filter(e -> e.role().equals(role))
            .collect(Collectors.toList());
    }
}
Enter fullscreen mode Exit fullscreen mode

They work effectively with Optional:

public record UserSettings(
    Optional<String> theme,
    Optional<Boolean> notifications
) {
    public UserSettings {
        theme = Objects.requireNonNullElse(theme, Optional.empty());
        notifications = Objects.requireNonNullElse(notifications, Optional.empty());
    }
}
Enter fullscreen mode Exit fullscreen mode

Records integrate well with modern frameworks like Spring:

@Configuration
public class AppConfig {
    record DatabaseConfig(String url, String username, String password) { }

    @Bean
    public DataSource dataSource(DatabaseConfig config) {
        return DataSourceBuilder.create()
            .url(config.url())
            .username(config.username())
            .password(config.password())
            .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Performance considerations are important. Records typically perform better than traditional classes for small, immutable data objects due to their simplified structure and final fields.

I've found records particularly useful in these scenarios:

  • API responses
  • Configuration objects
  • Test data structures
  • Event objects
  • Database query results
  • Validation results

While records offer many benefits, they're not suitable for every situation. They work best for pure data carriers where immutability is desired. Traditional classes remain better for objects with complex behavior or mutable state.

Remember these key points about records:

  • They're implicitly final
  • All fields are final
  • They can't extend other classes
  • They can implement interfaces
  • They support compact constructors
  • They generate equals(), hashCode(), and toString()

Records have transformed how I write data-centric code in Java, reducing errors and improving readability. They represent a significant step forward in Java's evolution, providing a powerful tool for handling immutable data structures.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)