ExerciseChallenge
Challenge
Make the record actually immutable
20 minintermediate2–10 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 StarterHints
Hint 1
Start with the List. One line in a compact constructor closes it.
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.
Hint 3
byte[] has no immutable form at all. clone() on both sides, and then look at what equals does with it.
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.