In Java, records are special classes introduced in Java 14 (as a preview feature) and became a standard feature in Java 16. They are specifically designed to model immutable data carriers with minimal boilerplate code. A record in Java automatically generates boilerplate code such as constructors, getters, toString, equals, and hashCode methods, making it a great choice for representing immutable data.
Here’s how you can use records as immutable data carriers:
1. Define a Record
To define a record, use the record keyword. A record automatically generates:
- A constructor.
- Accessors (getters) for all fields.
toString(),equals(), andhashCode()based on the fields.
Example:
public record User(String name, int age) {}
This creates an immutable User record class with:
- Fields:
nameandage - Automatically provides:
- Constructor:
User(String name, int age) name()andage()as accessors for the fields- A meaningful `toString(), method
- Implementations of
equals()andhashCode()
- Constructor:
2. Using a Record
Once defined, you can use the record class as follows:
public class Main {
public static void main(String[] args) {
// Creating and using a User record
User user = new User("Alice", 30);
// Access fields (no need for `getName()` or `getAge()`)
System.out.println(user.name()); // Alice
System.out.println(user.age()); // 30
// Automatic toString()
System.out.println(user); // User[name=Alice, age=30]
// Automatic equals() and hashCode()
User anotherUser = new User("Alice", 30);
System.out.println(user.equals(anotherUser)); // true
}
}
3. Immutability
Records are immutable by default:
- The fields of a record are implicitly
private final. - Once an object is created, its fields cannot be changed.
- Records make it easier to declare immutable objects compared to manually writing getters and using
final.
4. Customizing a Record
While records are concise, you can still customize them if needed:
- Add extra methods.
- Implement additional interfaces.
- Preprocess fields in the constructor or validate input.
Example:
public record User(String name, int age) {
public User {
// Compact constructor for validation
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
}
// Additional method
public String greeting() {
return "Hello, " + name + "!";
}
}
Usage:
User user = new User("Alice", 30);
System.out.println(user.greeting()); // Hello, Alice!
5. Limitations of Records
While records are extremely powerful for data carrier use cases, they are not suitable for every situation:
- Records cannot extend other classes (but they can implement interfaces).
- Fields in records cannot be modified after object creation.
- Records are designed primarily for data aggregation and are not meant for behavior-heavy classes.
6. Common Use Cases
- Representing DTOs (Data Transfer Objects).
- Creating immutable models for APIs.
- Storing simple structured data (e.g., key-value pairs, coordinates).
Summary
To use records for immutable data carriers:
- Define them with
record. The syntax automatically generates boilerplate code. - Use the generated constructors and field accessors (
name()instead ofgetName()). - Optionally, customize validation or add methods if you need additional behavior.
By leveraging records, you simplify your code, reduce boilerplate, and ensure your data class is immutable by design!
