Challenge

Make the record actually immutable

20 minintermediate210 yrs

Edge cases. You have to reason, and two valid fixes differ.

What this teaches

  • A compact constructor is where inbound defensive copies go
  • List.copyOf freezes a collection; nothing freezes an array
  • An array component also breaks equals, which compares identity not contents
  • Three mutable components, three different answers — the type decides the fix

Starter

Starter.javaOpen in playground
import java.util.*;

/**
 * CHALLENGE — 20 minutes.
 *
 * Three records, three mutable components, three different fixes. The
 * component's type decides which one you need, and the third has no clean
 * answer at all.
 *
 * TASKS
 *   1. Run it. All three "immutable" records are modified from outside.
 *   2. Fix Team with a compact constructor. One line.
 *   3. Fix Booking. Date cannot be frozen, so one line is not enough —
 *      work out which two you need and why.
 *   4. Fix Payload. There is no immutable array, so clone() on both sides.
 *      Then look at the equality check it prints and decide what to do.
 *   5. In a comment: state the rule for choosing between these three fixes
 *      by looking at a component's type.
 */
public class Starter {

    record Team(String name, List<String> members) {}

    record Booking(String reference, Date when) {}

    record Payload(String id, byte[] bytes) {}

    public static void main(String[] args) {
        System.out.println("── Team: a collection component ──");
        List<String> members = new ArrayList<>(List.of("ravi"));
        Team team = new Team("payments", members);
        members.add("added by the caller");
        team.members().add("added by a reader");
        System.out.println("  " + team);

        System.out.println();
        System.out.println("── Booking: a mutable legacy type ──");
        Date when = new Date(0);
        Booking booking = new Booking("BK-1", when);
        when.setTime(86_400_000L);
        System.out.println("  caller moved it   : " + booking.when().getTime());
        booking.when().setTime(172_800_000L);
        System.out.println("  reader moved it   : " + booking.when().getTime());

        System.out.println();
        System.out.println("── Payload: an array component ──");
        byte[] bytes = {1, 2, 3};
        Payload payload = new Payload("P-1", bytes);
        bytes[0] = 99;
        System.out.println("  caller edited it  : " + Arrays.toString(payload.bytes()));
        System.out.println("  two equal-content Payloads are equal : "
                + new Payload("P-1", new byte[] {1, 2, 3})
                        .equals(new Payload("P-1", new byte[] {1, 2, 3})));

        // Question to answer in a comment before you move on:
        // The last line is false even before you change anything. What does
        // a record's generated equals do with an array component, and what
        // does that mean for using Payload as a map key?
    }
}

Run it locally:

cd exercises/java/oop/record-vs-class/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    Start with the List. One line in a compact constructor closes it.

  2. Hint 2

    Date cannot be frozen. You need both halves — copy in, and copy out of the accessor — because the accessor hands out the only copy you have.

  3. Hint 3

    byte[] has no immutable form at all. clone() on both sides, and then look at what equals does with it.

  4. Hint 4

    Run the equality check on two Payloads with identical bytes. Is the result what a record promises?

Done when

  • The List component cannot be changed from outside, in either direction
  • The Date component cannot be changed from outside, in either direction
  • The byte[] component is cloned on both sides
  • A comment states what equals does with the byte[] and what you would do about it

Stretch

The byte[] case has no good ending: clone() fixes mutation and leaves equals and hashCode comparing array identity, so two Payloads with identical bytes are unequal and a record's central promise is broken. Decide what you would actually ship — override equals and hashCode, wrap the bytes in a small immutable type, or stop using a record — and write down the cost of the one you chose.

← Back to When should you use a record instead of a class?