Sitemap

Java Records and Builder pattern For Record

What are Java Records?

Press enter or click to view image in full size

Records are a special kind of class became a standard feature in Java 16. They are designed to be a simpler way to create data-carrying classes, reducing the boilerplate code associated with typical classes that only hold data.

Purpose of Java Records

Records automate the creation of methods such as equals(), hashCode(), toString(), and getters, which are often manually implemented in traditional classes.

public 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) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}

With a Record, the above class can be simplified to:

public record Person(String name, int age) {}

Syntax and Basic Usage

Defining a Record

A record is defined using the record keyword, followed by the name of the record and the list of fields.

Example:

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

Accessors

Java Records automatically generate accessor methods. The method names match the field names.

Point point = new Point(5, 10);
int x = point.x(); // 5
int y = point.y(); // 10

Immutability

All fields in a Record are implicitly final, meaning their values cannot be changed after the record is created.

Point point = new Point(5, 10);
point.x = 7; // Compilation error: cannot assign a value to final variable x

Constructors

Canonical Constructor

The canonical constructor is automatically provided, taking all fields as arguments.

Example:

Point point = new Point(5, 10);

Compact Constructors

Compact constructors allow for validation or other logic without explicitly declaring parameters.

Example:

public record Circle(double radius) {
public Circle {
if (radius <= 0) {
throw new IllegalArgumentException("Radius must be positive");
}
}
}

Custom Methods and Overrides

Custom Methods

You can add methods to Records like any other class.

Get sunimal malkakulage’s stories in your inbox

Join Medium for free to get updates from this writer.

Example:

public record ComplexNumber(double real, double imaginary) {
public double magnitude() {
return Math.sqrt(real * real + imaginary * imaginary);
}
}
//Records can be extended by static (final or non-final) fields
public record Person(String name, int age) {
private static final int DEFAULT_AGE = 0;

public Person(String name) {
this(name, DEFAULT_AGE);
}
}
/** Static method in a record */
public record Person(String name, int age) {
private static final int DEFAULT_AGE = 30;

public Person(String name) {
this(name, DEFAULT_AGE);
}
// Static method returns default age.
public static int getDefaultAge() {
return DEFAULT_AGE;
}
}

Overriding equals(), hashCode(), and toString()

Records automatically generate equals(), hashCode(), and toString() methods, but you can override them if needed.

public record Person(String name, int age) {
@Override
public String toString() {
return String.format("Person[name=%s, age=%d]", name, age);
}
}

Records can implement interfaces but cannot extend classes and since Records are implicitly final they cannot inherit from them also.

/** Implementing an interface */
public interface BaseShape {
int perimeter();
}
public record Rectangle(int width, int height) implements BaseShape {
}

Use Cases and Best Practices

public record Employee(String name, String position, double salary) {}

When to Use Records

Records are ideal for simple data carriers like DTOs (Data Transfer Objects), value objects, or entities in functional programming. They are best used when you need:

  • Immutable data objects.
  • Automatic implementations of equals(), hashCode(), and toString().
  • A concise and readable way to define such objects.

When Not to Use Records

Avoid using records when:

  • The object needs to be mutable.
  • You need to extend other classes (records cannot extend classes, though they can implement interfaces).
  • You require complex behavior or additional state management beyond simple data encapsulation.
// Not ideal as a record due to required mutability or complex behavior
public class BankAccount {
private double balance;

public void deposit(double amount) {
balance += amount;
}

public void withdraw(double amount) {
balance -= amount;
}
}

Comparison with Other Java Constructs

Versus Regular Classes

Records significantly reduce boilerplate code compared to regular classes.

Example:

// Regular class
public class Point {
private final int x;
private final int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

// Getters, equals(), hashCode(), toString() ...
}

// Record equivalent
public record Point(int x, int y) {}

Versus Lombok’s @Data Annotation

Lombok’s @Data annotation can also reduce boilerplate code, but it requires an external library. Records are built into the Java language and offer better immutability guarantees.

import lombok.Data;

@Data
public class Person {
private final String name;
private final int age;
}

Versus Java Beans

Java Beans are typically mutable and require explicit getter/setter methods. They are more verbose and not as well-suited for modern, functional programming styles.

Serialization

Records can be serialized like any other Java object. If a record implements Serializable

import java.io.Serializable;

public record Person(String name, int age) implements Serializable {}

Pattern Matching with Records

Starting with Java 16, you can use records in pattern matching, which simplifies type checking and casting:

if (obj instanceof Person(String name, int age)) {
System.out.println(name + " is " + age + " years old.");
}

Creating a Builder for a Record

public record Person(String name, int age, String address) {}

Builder pattern is a design pattern that allows for a more flexible way of constructing objects in Java. While Java records themselves are immutable and provide a canonical constructor by default, there are cases where you might want to build a record in a more incremental or flexible manner, similar to how builders are used with regular classes that allows for a more flexible way of constructing immutable records in Java

Create the Builder Class

You can create a nested static builder class within the record:

public record Person(String name, int age, String address) {

public static class Builder {
private String name;
private int age;
private String address;

public Builder name(String name) {
this.name = name;
return this;
}

public Builder age(int age) {
this.age = age;
return this;
}

public Builder address(String address) {
this.address = address;
return this;
}

public Person build() {
return new Person(name, age, address);
}
}
}

Using the Builder Class

With the builder class in place, constructing a Person record becomes more flexible and readable, especially when dealing with records that have many fields:

public class Main {
public static void main(String[] args) {
Person person = new Person.Builder()
.name("Alice")
.age(30)
.address("123 Main St")
.build();

System.out.println(person);
//OutPut: Person[name=Alice, age=30, address=123 Main St]
}
}

Advantages of Using a Builder for Records

  1. Clarity: The builder pattern provides a more readable way of constructing objects, especially when there are multiple fields.
  2. Flexibility: The builder allows you to set fields incrementally, which can be useful when dealing with optional fields or when constructing an object in a non-linear fashion.
  3. Immutability: Even though you’re using a builder to set fields, the resulting Person object is still immutable, adhering to the principles of records.
  4. Validation: You can add validation logic in the build() method or within individual setter methods in the builder, ensuring that only valid objects are constructed.

While records are meant to be simple and straightforward, adding a builder class can make them more versatile, especially in more complex object construction scenarios. The builder pattern can make your code cleaner and easier to maintain, without sacrificing the immutability and simplicity that records provide.

Code Snippets Use cases

public record Book(String title, String author, double price) {}

public class Main {
public static void main(String[] args) {
Book book = new Book("Effective Java", "Joshua Bloch", 45.0);
System.out.println(book.title()); // Output: Effective Java
System.out.println(book); // Output: Book[title=Effective Java, author=Joshua Bloch, price=45.0]
}
}
public record Employee(String name, double salary) {
public Employee {
if (salary < 0) throw new IllegalArgumentException("Salary cannot be negative");
}

public double annualBonus() {
return salary * 0.10;
}
}

public class Main {
public static void main(String[] args) {
Employee emp = new Employee("Jane", 80000);
System.out.println(emp.name()); // Output: Jane
System.out.println(emp.annualBonus()); // Output: 8000.0
}
}