Records as Value Objects

OOP · lesson 27 of 43 · 3 min read

How records generate equals, hashCode and toString, and where validation goes.

Open this lesson in the learning hub

Key points

  • A record lists its components; Java writes the constructor, accessors, equals, hashCode and toString.
  • Equality is by value: two Money with the same components are equal, so they work as one key in a map or set.
  • The compact constructor Money { ... } is where you validate or normalise, before the fields are assigned.
  • Records are implicitly final and their fields are final, but a component holding a List is still mutable inside.
  • You can add your own methods, implement interfaces, and override any generated method that needs different behaviour.
  • Use a record when the identity of the object IS its data. Use a class when it has a lifecycle or hidden state.

Example

import java.util.List;
import java.util.Map;

public class Main {
    record Money(String currency, long cents) implements Comparable<Money> {
        Money {                                        // compact constructor: guard here
            if (cents < 0) throw new IllegalArgumentException("cents must be >= 0");
            currency = currency.toUpperCase();         // normalising is allowed
        }

        Money plus(Money other) {                      // return a NEW value, never mutate
            return new Money(currency, cents + other.cents);
        }

        @Override public int compareTo(Money o) { return Long.compare(cents, o.cents); }
    }

    public static void main(String[] args) {
        Money a = new Money("usd", 1250);
        Money b = new Money("USD", 1250);

        System.out.println(a);                             // generated toString
        System.out.println("equal?   " + a.equals(b));     // generated equals, by value
        System.out.println("in map?  " + Map.of(a, "invoice-1").containsKey(b));
        System.out.println("sum      " + a.plus(b));
        System.out.println("sorted   " + List.of(a, new Money("USD", 300)).stream().sorted().toList());

        try {
            new Money("USD", -1);
        } catch (IllegalArgumentException e) {
            System.out.println("rejected: " + e.getMessage());
        }
    }
}

If the data is the identity, make it a record and validate in the compact constructor.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OOP course, and every lesson in it is listed on the OOP contents page.