Skip to content
Part IA Michaelmas Term

2022 Paper 1 Question 3 — Immutability and AssetLocation Design

Question

(a) Give three advantages and one disadvantage of immutable classes. [4 marks]

(b) A programmer has created an AssetLocation class to represent the location of company assets. Some assets are mobile and their location must be updated regularly (e.g. vehicles); others are static and will never be updated (e.g. warehouses). The class contains a String describing the asset, an int recording a unique identifier, and two double values to represent valid latitude (90ϕ90-90^\circ \leq \phi \leq 90^\circ) and longitude (180<θ180-180^\circ < \theta \leq 180^\circ) values, respectively. All fields are initially mutable and set by the constructor.

Write Java code that implements AssetLocation as described. [5 marks]

(c) The programmer wishes to make the objects representing static assets immutable. They make the class optionally immutable using a parameter passed into the constructor.

  • (i) Write a modified AssetLocation class that implements the behaviour as described. [3 marks]
  • (ii) Explain why this is not a good solution. [3 marks]
  • (iii) Propose a better structure for the class, and explain your design choices. [5 marks]

Worked Solution

(a) Three advantages and one disadvantage of immutable classes — 4 marks

Model answer:

Advantages:

  1. Thread safety — Immutable objects are inherently thread-safe. Since their state never changes after construction, multiple threads can access them concurrently without synchronisation, locks, or data races. This eliminates a whole class of concurrency bugs.

  2. Simpler reasoning — You can rely on an immutable object’s state never changing. Once constructed, you can pass it anywhere, store it in collections, or use it as a map key without worrying that some other part of the code will mutate it out from under you. The object’s invariants are established once in the constructor and never need to be re-verified.

  3. Safe sharing and caching — Immutable objects can be freely shared (no defensive copying needed) and cached (a single instance can serve all callers, since no caller can corrupt it for others). The JVM can also perform optimisations (e.g., String interning for string literals).

Disadvantage:

  1. Performance overhead for frequent state changes — If an object’s state needs to change frequently (e.g., a counter, a moving vehicle’s position), immutability requires creating a new object on every “change.” This produces allocation pressure and GC overhead. A mutable object that is updated in place would be more efficient. (However, this is often overstated — modern JVMs handle short-lived objects efficiently.)

Why 4 marks: Three distinct advantages (thread safety, simpler reasoning, safe sharing) and one concrete disadvantage (allocation overhead). The question asks for “one disadvantage,” so give one strong one — not a vague “sometimes it is inconvenient.”

(b) Mutable AssetLocation — 5 marks

Model answer:

public class AssetLocation {
    private String description;
    private final int id;            // unique identifier — should never change
    private double latitude;
    private double longitude;

    public AssetLocation(String description, int id,
                         double latitude, double longitude) {
        this.description = description;
        this.id = id;
        setLatitude(latitude);
        setLongitude(longitude);
    }

    public String getDescription() { return description; }
    public int getId() { return id; }
    public double getLatitude() { return latitude; }
    public double getLongitude() { return longitude; }

    public void setDescription(String description) {
        this.description = description;
    }

    public void setLatitude(double latitude) {
        if (latitude < -90.0 || latitude > 90.0) {
            throw new IllegalArgumentException(
                "Latitude must be between -90 and 90 degrees, got: " + latitude);
        }
        this.latitude = latitude;
    }

    public void setLongitude(double longitude) {
        if (longitude <= -180.0 || longitude > 180.0) {
            throw new IllegalArgumentException(
                "Longitude must be > -180 and <= 180 degrees, got: " + longitude);
        }
        this.longitude = longitude;
    }
}

Key design decisions for the 5 marks:

  1. id is final — A unique identifier should never change. Making it final enforces this invariant. The question says “all fields are initially mutable,” but id as a unique identifier makes no sense as mutable — making it final shows design judgement.

  2. Validation in the constructor and setters — The valid ranges for latitude and longitude are specified (90ϕ90-90 \leq \phi \leq 90, 180<θ180-180 < \theta \leq 180). Validate on construction and on every mutation. Use IllegalArgumentException with a descriptive message.

  3. Encapsulation — Fields are private with public getters and setters. This allows future changes (e.g., adding a bounds check, changing the internal representation) without affecting clients.

  4. Getters present — Even for mutable fields, provide getters rather than exposing the fields directly.

(c)(i) Optionally immutable via constructor parameter — 3 marks

Model answer:

public class AssetLocation {
    private String description;
    private final int id;
    private double latitude;
    private double longitude;
    private final boolean immutable;

    public AssetLocation(String description, int id,
                         double latitude, double longitude,
                         boolean immutable) {
        this.description = description;
        this.id = id;
        setLatitude(latitude);
        setLongitude(longitude);
        this.immutable = immutable;
    }

    public void setDescription(String description) {
        if (immutable) throw new UnsupportedOperationException("Object is immutable");
        this.description = description;
    }

    public void setLatitude(double latitude) {
        if (immutable) throw new UnsupportedOperationException("Object is immutable");
        if (latitude < -90.0 || latitude > 90.0) {
            throw new IllegalArgumentException("Latitude out of range: " + latitude);
        }
        this.latitude = latitude;
    }

    public void setLongitude(double longitude) {
        if (immutable) throw new UnsupportedOperationException("Object is immutable");
        if (longitude <= -180.0 || longitude > 180.0) {
            throw new IllegalArgumentException("Longitude out of range: " + longitude);
        }
        this.longitude = longitude;
    }
}

(c)(ii) Why this is not a good solution — 3 marks

Model answer:

  1. Runtime failure, not compile-time safety — The immutability guarantee only surfaces at runtime (an UnsupportedOperationException). A caller with a reference typed as AssetLocation cannot tell from the type system whether mutation is safe — the setLatitude() method is always present in the API. This means (a) you cannot statically verify correctness, (b) bugs from accidentally mutating an immutable asset manifest only when the code runs.

  2. Conditional logic in every mutator — Every setter must check the immutable flag. This is repetitive boilerplate. A new developer adding a setter might forget the check, creating a loophole. The class has two reasons to change — business logic (asset location representation) and the immutable/mutable toggle — violating the Single Responsibility Principle.

  3. Confusing API — The class has setters that sometimes work and sometimes throw. This is a violation of the Liskov Substitution Principle at the conceptual level: you cannot treat all AssetLocation objects uniformly. Code that receives an AssetLocation must defensively check or assume the worst.

(c)(iii) Better structure — 5 marks

Model answer: Separate the mutable and immutable variants into an interface and two implementations, or use an abstract base class with two concrete subclasses.

Option A: Interface-based approach

public interface AssetLocation {
    String getDescription();
    int getId();
    double getLatitude();
    double getLongitude();
}

public final class ImmutableAssetLocation implements AssetLocation {
    private final String description;
    private final int id;
    private final double latitude;
    private final double longitude;

    public ImmutableAssetLocation(String description, int id,
                                  double latitude, double longitude) {
        this.description = description;
        this.id = id;
        if (latitude < -90.0 || latitude > 90.0) {
            throw new IllegalArgumentException("Latitude out of range: " + latitude);
        }
        if (longitude <= -180.0 || longitude > 180.0) {
            throw new IllegalArgumentException("Longitude out of range: " + longitude);
        }
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public String getDescription() { return description; }
    public int getId() { return id; }
    public double getLatitude() { return latitude; }
    public double getLongitude() { return longitude; }
}

public class MutableAssetLocation implements AssetLocation {
    private String description;
    private final int id;
    private double latitude;
    private double longitude;

    public MutableAssetLocation(String description, int id,
                                double latitude, double longitude) {
        this.description = description;
        this.id = id;
        setLatitude(latitude);
        setLongitude(longitude);
    }

    public String getDescription() { return description; }
    public int getId() { return id; }
    public double getLatitude() { return latitude; }
    public double getLongitude() { return longitude; }

    public void setDescription(String description) {
        this.description = description;
    }

    public void setLatitude(double latitude) {
        if (latitude < -90.0 || latitude > 90.0) {
            throw new IllegalArgumentException("Latitude out of range: " + latitude);
        }
        this.latitude = latitude;
    }

    public void setLongitude(double longitude) {
        if (longitude <= -180.0 || longitude > 180.0) {
            throw new IllegalArgumentException("Longitude out of range: " + longitude);
        }
        this.longitude = longitude;
    }
}

Option B: Copy-on-mutation (if the interface must have mutation methods)

Provide mutation methods that return new instances:

public final class AssetLocation {
    private final String description;
    private final int id;
    private final double latitude;
    private final double longitude;

    // ... constructor and getters ...

    public AssetLocation withLatitude(double newLat) {
        return new AssetLocation(description, id, newLat, longitude);
    }

    public AssetLocation withLongitude(double newLon) {
        return new AssetLocation(description, id, latitude, newLon);
    }
}

Design justification (for the 5 marks):

  1. Interface defines the contractAssetLocation as an interface captures what all locations have in common (a description, an ID, and coordinates). Client code can work with the interface without knowing mutability.

  2. ImmutableAssetLocation is final — Preventing subclassing ensures the immutability guarantee cannot be broken by a subclass adding mutable fields.

  3. No UnsupportedOperationException — The immutable variant simply has no setters. The type system guarantees safety — there is no runtime surprise.

  4. MutableAssetLocation adds mutation — The setters only exist on the mutable variant. Code that holds an AssetLocation reference cannot mutate; code that needs mutation uses the concrete MutableAssetLocation type or is passed one.

  5. Satisfies OCP and LSP — New variants (e.g., a CachedAssetLocation that wraps a remote lookup) can be added by implementing the interface. Any code that works with AssetLocation works correctly with any implementation.

  6. Clear intent — The type name tells you the contract: ImmutableAssetLocation cannot change, MutableAssetLocation can. No hidden boolean flag to inspect.

Why 5 marks: Propose a concrete design (either interface + two classes, or the final immutable copy-on-mutation approach), explain the type-safety benefit, connect to design principles (LSP, OCP, SRP), and address why the constructor-flag approach is inferior.