Immutable Classes and Defensive Copies
Create truly immutable Java classes with defensive copies, unmodifiable collections, and builder pattern.
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.
Java Optional — Avoid NullPointerException
Use Optional to handle nullable values safely with map, flatMap, orElse, and ifPresent patterns.
Best for: Eliminating NullPointerException in business logic
Try-With-Resources and AutoCloseable
Manage resources safely with try-with-resources: files, connections, streams, and custom resources.
Best for: Safe resource management preventing leaks
Java Enums — Methods, Fields, and Interfaces
Advanced enum patterns: enums with fields, abstract methods, implementing interfaces, and enum maps.
Best for: Type-safe constants with behavior
Custom Exception Hierarchy
Design a clean exception hierarchy with custom exceptions, error codes, and exception mapping.
Best for: Consistent error handling across services