javaintermediate

Immutable Classes and Defensive Copies

Create truly immutable Java classes with defensive copies, unmodifiable collections, and builder pattern.

java
import java.util.*;
import java.time.LocalDate;

// Immutable class — all fields final, no setters, defensive copies
public final class Order {
    private final String id;
    private final String customer;
    private final LocalDate date;
    private final List<LineItem> items;
    private final double total;

    private Order(Builder builder) {
        this.id = builder.id;
        this.customer = builder.customer;
        this.date = builder.date;
        this.items = List.copyOf(builder.items); // defensive copy
        this.total = builder.items.stream()
            .mapToDouble(i -> i.price() * i.qty())
            .sum();
    }

    // Only getters, no setters
    public String id() { return id; }
    public String customer() { return customer; }
    public LocalDate date() { return date; }
    public List<LineItem> items() { return items; } // already unmodifiable
    public double total() { return total; }

    // "With" methods for derived copies
    public Order withCustomer(String customer) {
        return builder()
            .id(this.id)
            .customer(customer)
            .date(this.date)
            .items(this.items)
            .build();
    }

    public static Builder builder() { return new Builder(); }

    // Builder pattern for construction
    public static class Builder {
        private String id;
        private String customer;
        private LocalDate date = LocalDate.now();
        private List<LineItem> items = new ArrayList<>();

        public Builder id(String id) { this.id = id; return this; }
        public Builder customer(String c) { this.customer = c; return this; }
        public Builder date(LocalDate d) { this.date = d; return this; }
        public Builder items(List<LineItem> i) { this.items = new ArrayList<>(i); return this; }
        public Builder addItem(LineItem i) { this.items.add(i); return this; }

        public Order build() {
            Objects.requireNonNull(id, "id required");
            Objects.requireNonNull(customer, "customer required");
            if (items.isEmpty()) throw new IllegalStateException("Order must have items");
            return new Order(this);
        }
    }

    // Immutable value object (record handles this automatically)
    record LineItem(String product, int qty, double price) {
        LineItem {
            if (qty <= 0) throw new IllegalArgumentException("qty must be positive");
            if (price < 0) throw new IllegalArgumentException("price must be non-negative");
        }
    }

    public static void main(String[] args) {
        Order order = Order.builder()
            .id("ORD-001")
            .customer("Alice")
            .addItem(new LineItem("Widget", 3, 9.99))
            .addItem(new LineItem("Gadget", 1, 49.99))
            .build();

        System.out.printf("Order %s: $%.2f%n", order.id(), order.total());
        // order.items().add(...) → UnsupportedOperationException
    }
}

Use Cases

  • Thread-safe domain objects
  • Value objects in domain-driven design
  • Fluent object construction with validation

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.