Challenge

Close both directions

20 minintermediate210 yrs

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

What this teaches

  • Collections.unmodifiableList is a view of a list somebody else can change
  • List.copyOf severs the connection, and is free when the input is already immutable
  • Arrays.asList is a fixed-size view of the array — set works, add throws
  • Each component type needs a different technique, and some have none

Starter

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

/**
 * CHALLENGE — 20 minutes.
 *
 * Part 1 is a measurement: four ways of "protecting" a list, and they do
 * four different things. Part 2 is the fix that follows from it.
 *
 * TASKS
 *   1. Run part 1 and note which of the four reflects a later change to the
 *      source, and which of them refuses writes.
 *   2. Note what List.copyOf returned for an input that was already
 *      immutable. That fact answers the "copying is too slow" objection.
 *   3. Make every assertion in part 2 print true, by fixing Shipment only.
 *      Each of the three components needs a different technique.
 *   4. In a comment: why does Arrays.asList allow set but not add?
 */
public class Starter {

    static final class Shipment {
        private final String reference;
        private final List<String> parcels;
        private final Date dispatchedAt;

        Shipment(String reference, List<String> parcels, Date dispatchedAt) {
            this.reference = reference;
            this.parcels = parcels;
            this.dispatchedAt = dispatchedAt;
        }

        String reference() { return reference; }

        List<String> parcels() { return parcels; }

        Date dispatchedAt() { return dispatchedAt; }
    }

    public static void main(String[] args) {
        System.out.println("── part 1: four kinds of 'protected' ──");

        List<String> source = new ArrayList<>(List.of("first"));

        List<String> view = Collections.unmodifiableList(source);
        List<String> copy = List.copyOf(source);
        List<String> mutableCopy = new ArrayList<>(source);
        List<String> arrayView = Arrays.asList("a", "b");

        source.add("added later");

        System.out.println("  unmodifiableList sees it : " + view);
        System.out.println("  List.copyOf sees it      : " + copy);
        System.out.println("  new ArrayList sees it    : " + mutableCopy);
        System.out.println("  copyOf of an immutable list is the same instance : "
                + (List.copyOf(copy) == copy));

        arrayView.set(0, "z");
        System.out.println("  Arrays.asList after set  : " + arrayView);
        try {
            arrayView.add("c");
        } catch (UnsupportedOperationException e) {
            System.out.println("  Arrays.asList add        : UnsupportedOperationException");
        }

        System.out.println();
        System.out.println("── part 2: fix Shipment ──");

        List<String> parcels = new ArrayList<>(List.of("P-1"));
        Date dispatched = new Date(1_700_000_000_000L);
        Shipment shipment = new Shipment("SH-1", parcels, dispatched);

        int parcelsAtStart = shipment.parcels().size();
        long timeAtStart = shipment.dispatchedAt().getTime();

        // Four attacks. All four should leave the shipment untouched.
        parcels.add("added by the caller");
        dispatched.setTime(0);
        try { shipment.parcels().add("added by a reader"); } catch (UnsupportedOperationException ignored) { }
        shipment.dispatchedAt().setTime(0);

        boolean parcelsIntact = shipment.parcels().size() == parcelsAtStart;
        boolean timeIntact = shipment.dispatchedAt().getTime() == timeAtStart;

        System.out.println("  parcels unchanged : " + parcelsIntact);
        System.out.println("  dispatch unchanged: " + timeIntact);
        System.out.println();
        System.out.println(parcelsIntact && timeIntact
                ? "Shipment is immutable" : "Shipment is NOT immutable yet");
    }
}

Run it locally:

cd exercises/java/oop/immutability-in-practice/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    Run the view-vs-copy comparison first. One of them changes under you.

  2. Hint 2

    Check whether List.copyOf allocates when handed a List.of — the answer decides whether the 'too slow' objection is real.

  3. Hint 3

    For the Date field you need both halves. Ask what the getter is handing out and whether there is a second copy anywhere.

  4. Hint 4

    Arrays.asList: try set and then add on the same list, and explain the difference from what it is a view OF.

Done when

  • Every assertion in main() prints true
  • No component can be reached or changed from outside, in either direction
  • A comment records what List.copyOf returned for an already-immutable input
  • A comment explains why Arrays.asList allows set but not add

Stretch

Add a component whose type you do not control and which is mutable with no copy constructor — assume a third-party class with public fields. Work out what your options are: wrap it in your own immutable view, copy it field-by-field, or accept the leak and document it. Pick one and say what you would write in the class javadoc so the next reader knows.

← Back to How do you make a class genuinely immutable?