Java Records and Builder pattern For Record
What are Java Records?
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(); // 10Immutability
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 xConstructors
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(), andtoString(). - 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
- Clarity: The builder pattern provides a more readable way of constructing objects, especially when there are multiple fields.
- 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.
- Immutability: Even though you’re using a builder to set fields, the resulting
Personobject is still immutable, adhering to the principles of records. - 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
}
}
![Ja[Var]](https://miro.medium.com/v2/resize:fit:1358/format:webp/1*YGQhwqCH9F-jVwbLwjgA9g.png)







