Java Records: A Definitive Guide

Passing immutable data between objects is one of the most common yet mundane tasks in Java applications. Before Java 14, this often required creating classes with fields and methods that were prone to errors and obscured the class’s primary purpose. Java Records, introduced in Java 14 and fully adopted in Java 16, provide a more concise and expressive way to handle immutable data classes.

In this article, we’ll explore the fundamentals of records, their purpose, generated methods and some customization techniques.

The Purpose of Java Records

In many Java applications, we often create classes to hold simple data, like database results, query results, or information from external services. These data classes typically need to be immutable to ensure data integrity without requiring complex synchronization.

To achieve this, a traditional Java data class would include:

  • Private, final fields for each piece of data.
  • Getter methods for each field.
  • A public constructor that takes arguments for each field.
  • An equals method to compare objects based on field values.
  • A hashCode method that returns consistent values for matching field values.
  • A toString method that provides a readable representation of the class.

For example, here’s a basic Product class implemented in the traditional way:

import java.util.Objects;
public class Product {
    private final String name;
    private final double price;
    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (!(obj instanceof Product)) {
            return false;
        } else {
            Product other = (Product) obj;
            return Objects.equals(name, other.name) && Double.compare(price, other.price) == 0;
        }
    }
    @Override
    public String toString() {
        return "Product [name=" + name + ", price=" + price + "]";
    }
    // standard getters
}

Drawbacks of the Traditional Approach:

  1. Boilerplate Code: Repeating the same pattern for each data class, writing constructors, getters, equals, hashCode, and toString methods.
  2. Obscured Intent: The primary purpose of the class—holding data—is lost amidst the boilerplate code.

Java Records address these issues by explicitly declaring a class as a data class, removing the boilerplate and focusing on the data itself.

The Basics of Java Records

As of JDK 14, records allow us to replace repetitive data classes with a more compact and readable syntax. A record automatically provides implementations for equals, hashCode, toString, and a constructor, all based on the fields you declare.

To create a Product record, you would write:

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

This single line achieves the same functionality as the previous example but in a much more concise form.

Constructor in Records

With records, Java automatically generates a public constructor that accepts all the fields as arguments:

public Product(String name, double price) {
    this.name = name;
    this.price = price;
}

You can use this constructor to create instances just as you would with a traditional class:

Product product = new Product("Laptop", 999.99);
System.out.println(product); // Output: Product[name=Laptop, price=999.99]
Getters in Records

Records provide public getter methods for each field, named after the fields themselves:

String name = product.name();   // Returns "Laptop"
double price = product.price(); // Returns 999.99
Automatically Generated Methods

Records come with automatically generated methods for equals, hashCode, and toString, saving you from writing these methods manually.

equals

The equals method compares two record instances and returns true if they have the same type and their field values match:

Product product1 = new Product("Laptop", 999.99);
Product product2 = new Product("Laptop", 999.99);
System.out.println(product1.equals(product2)); // Output: true
hashCode

The hashCode method returns the same hash value for two record instances if all their field values match:

System.out.println(product1.hashCode() == product2.hashCode()); // Output: true
toString

The toString method provides a string representation of the record, showing the class name and field values:

System.out.println(product); // Output: Product[name=Laptop, price=999.99]

Customizing Java Records

While records provide much functionality automatically, they also allow for customization, such as adding validation logic or defining additional methods.

Custom Constructors

Records support custom constructors where you can add validation logic. For instance, to ensure that the name is not null and price is positive:

public record Product(String name, double price) {
    public Product {
        Objects.requireNonNull(name, "Name cannot be null");
        if (price < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }
    }
}

You can also define additional constructors with different argument lists:

public record Product(String name, double price) {
    public Product(String name) {
        this(name, 0.0);
    }
}
Static Methods and Fields

Records can include static variables and methods just like regular classes:

public record Product(String name, double price) {
    public static final String DEFAULT_NAME = "Unknown Product";
    public static Product defaultProduct() {
        return new Product(DEFAULT_NAME, 0.0);
    }
}
public class Main {
    public static void main(String[] args) {
        Product defaultProduct = Product.defaultProduct();
        System.out.println(defaultProduct); // Output: Product[name=Unknown Product, price=0.0]
    }
}

Java Records simplify the creation of immutable data classes by eliminating boilerplate code and focusing on the data. By using records, you gain automatic implementations of common methods like equals, hashCode, and toString, making your code cleaner, more readable, and less error-prone.

If you’re working with data-centric classes in Java, consider using records to streamline your development process and improve the clarity and maintainability of your code. They offer a modern approach to handling immutable data, making Java applications more concise and expressive.

Stay tuned for more new updates regarding newer Java versions, don’t forget to subscribe to receive all new articles and Java tips !


Discover more from Byte Code

Subscribe to get the latest posts sent to your email.

Leave a Reply

I’m A Java Enthusiast

Welcome to Byte-Code, your go-to corner of the internet for all things Java. Here, I invite you to join me on a journey through the world of programming, where we’ll explore the intricacies of Java, dive into coding challenges, and build solutions with a touch of creativity. Let’s code something amazing together!

Let’s connect