Java Record: A Comprehensive Guide with Examples

Java Records, introduced in Java 14 as a preview feature and becoming a standard feature in Java 16, provide a concise way to model immutable data. This article explores Java Records in detail, comparing them with traditional classes, and providing practical examples and insights into their usage.

Java Record

1. Introduction to Java Records

Java Records are a special kind of class in Java designed to act as simple data carriers. They reduce the boilerplate code required for creating POJOs (Plain Old Java Objects) by automatically generating methods like equals(), hashCode(), and toString(), as well as getters for all fields.

2. Java Record Example

Here’s a basic example of a Java Record:

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

This single line of code defines a class Person with two fields: name and age. The compiler automatically generates the following methods:

  • Constructor
  • Getters
  • equals()
  • hashCode()
  • toString()

3. Java Record vs Class

Traditional classes require explicit declaration of fields, constructors, getters, and overrides for equals(), hashCode(), and toString(). Records, however, handle this automatically.

Class Example:

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 getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

In this class example, you have to manually create constructors, getters, and override equals(), hashCode(), and toString() methods.

Record Example:

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

In the record example, all the necessary methods are automatically generated, significantly reducing the amount of boilerplate code.

4. Java Record Type

Records are a new type in Java specifically designed to model data immutably. They are final and implicitly extend java.lang.Record. This means you cannot extend a record class.

5. Java Record Default Value

Records do not have default values. All fields must be initialized through the constructor provided by the record. Attempting to use a record without initializing all fields will result in a compilation error.

6. Java Record Constructor

Records automatically generate a canonical constructor matching the fields declared. However, you can also provide a custom constructor if needed.

Default Constructor:

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

Custom Constructor:

public record Person(String name, int age) {
    public Person {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}

In the custom constructor, you can add additional logic, such as validation checks. In this example, the constructor throws an IllegalArgumentException if the age is negative.

7. Java Record Inheritance

Records cannot extend other classes because they implicitly extend java.lang.Record. This design choice ensures that records remain simple and focused on being data carriers.

8. Java Record Implement Interface

Records can implement interfaces, making them flexible for various use cases.

public interface Greetable {
    String greet();
}

public record Person(String name, int age) implements Greetable {
    @Override
    public String greet() {
        return "Hello, my name is " + name;
    }
}

In this example, the Person record implements the Greetable interface, providing a custom implementation for the greet method.

9. Java Record Equals and HashCode

Records automatically provide equals() and hashCode() methods based on their fields.

Example:

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

// Usage
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
System.out.println(p1.equals(p2)); // true

The equals method in records compares the field values of the two objects, making p1 and p2 equal if their name and age fields are the same.

10. Java Record Extends

Records cannot extend any other class besides java.lang.Record. This ensures that records remain focused on their primary purpose: storing immutable data.

11. Java Record vs Lombok

Lombok provides annotations to reduce boilerplate in traditional classes. Records inherently reduce boilerplate without additional annotations.

Lombok Example:

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

In this example, Lombok annotations @Data and @AllArgsConstructor are used to generate getters, setters, and constructors, among other methods.

Record Example:

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

In the record example, no annotations are needed to achieve the same result, making the code cleaner and more concise.

12. Java Record ToString

Records provide a default toString() implementation that includes the class name and field values.

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

// Usage
Person person = new Person("Alice", 30);
System.out.println(person); // Person[name=Alice, age=30]

The default toString method output includes the class name and the values of the fields in a readable format.

13. Java Record Validation

Validation logic can be added in the canonical constructor.

public record Person(String name, int age) {
    public Person {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}

In this example, the custom constructor ensures that the age is not negative, adding a layer of data validation.

14. Java Record Multiple Constructors

Records can have additional constructors, but they must delegate to the canonical constructor.

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

Here, an additional constructor allows creating a Person with a default age of 0 if only the name is provided.

15. Java Record Optional Field

Optional fields can be handled using java.util.Optional.

public record Person(String name, Optional<String> address) {
    public Person(String name) {
        this(name, Optional.empty());
    }
}

In this example, the address field is optional. The second constructor provides a default empty Optional if no address is provided.

Conclusion

Java Records offer a succinct way to define data-holding classes with minimal boilerplate. They are a powerful feature for creating immutable objects, enhancing readability, and reducing error-prone code. Understanding and leveraging records can significantly improve the efficiency and clarity of your Java applications.

Java 17 features you can checkout here.


References

Share this article with tech community
WhatsApp Group Join Now
Telegram Group Join Now

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply

    Your email address will not be published. Required fields are marked *