Loading content...
You've learned the mechanics of defensive copying—how to protect both inputs and outputs. But a master craftsperson knows not just how to use their tools, but when. Defensive copying everywhere would be wasteful; defensive copying nowhere would be dangerous.
This page provides the decision framework that experienced engineers use to determine when defensive copying is necessary, when it's overkill, and when alternative approaches are preferable. By the end, you'll be able to make these judgments quickly and confidently in real-world code.
By the end of this page, you will understand: the trust boundary concept that guides defensive copying decisions, situations that always require defensive copying, situations where it can be safely skipped, alternatives to defensive copying, and a practical decision checklist for everyday use.
The fundamental question for defensive copying is: Do you trust the code you're interacting with?
A trust boundary is a conceptual line separating code you control and trust from code you don't control or don't trust. Defensive copying is primarily needed when data crosses trust boundaries.
12345678910111213141516171819
TRUST BOUNDARIES IN A TYPICAL SYSTEM ┌─────────────────────────────────────────────────────────────────┐│ UNTRUSTED ZONE ││ External clients, third-party code, user input, plugins ││ ││ ══════════════════ TRUST BOUNDARY ═══════════════════════════ ││ ││ YOUR PUBLIC API ││ Public methods, API endpoints, library interfaces ││ ↓ Defensive copying needed here ↓ ││ ││ ────────────────────────────────────────────────────────────── ││ ││ TRUSTED ZONE ││ Your internal implementation, private methods, team's code ││ Defensive copying often unnecessary within this zone ││ │└─────────────────────────────────────────────────────────────────┘Why trust matters:
Untrusted code might mutate objects inadvertently or maliciously. It's outside your control, and you cannot predict its behavior. Examples: third-party library callers, user input, plugins, code written by different teams.
Trusted code follows the same conventions and is under the same code review. You can rely on it not to violate assumptions. Examples: private helper methods, internal classes, your own carefully documented modules.
The key insight: Defensive copying is the mechanism that enforces trust boundaries. Without it, you're hoping untrusted code behaves correctly rather than ensuring it cannot cause harm.
Even if you control all current callers, consider future maintainers. Three years from now, someone (maybe you) might call your API in unexpected ways. Public and protected APIs should be defensive against future misuse, not just current usage.
Certain situations demand defensive copying regardless of current callers or performance concerns. These are non-negotiable for professional code.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// EXAMPLE 1: Public API — always copypublic class PublicLibraryApi { public void processData(List<Record> records) { // Public API: caller is untrusted List<Record> safeCopy = records.stream() .map(Record::new) .collect(Collectors.toList()); processInternal(safeCopy); }} // EXAMPLE 2: Security-sensitive — always copypublic class UserSession { private final Set<Permission> permissions; public UserSession(Set<Permission> permissions) { // Security-critical: must be fully controlled this.permissions = Set.copyOf(permissions); } public Set<Permission> getPermissions() { // Never expose mutable security state return Set.copyOf(permissions); }} // EXAMPLE 3: Documented immutability — always copy/** * An immutable time range. * Thread-safe and safe to share. */public final class TimeRange { private final Date start; private final Date end; public TimeRange(Date start, Date end) { // Contract promises immutability: must copy this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); }} // EXAMPLE 4: Collection keys — always copypublic class UserCache { private final Map<UserId, UserData> cache = new HashMap<>(); public void cacheUser(UserId id, UserData data) { // UserId is used as key: must not change UserId keyCopy = new UserId(id); // Defensive copy cache.put(keyCopy, data); }}Not every method needs defensive copying. Over-copying wastes resources and obscures intent. Here's when you can safely skip it.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// EXAMPLE 1: Immutable types — no copy neededpublic class Order { private final String orderId; // String is immutable private final LocalDate orderDate; // LocalDate is immutable private final BigDecimal totalAmount; // BigDecimal is immutable public Order(String orderId, LocalDate orderDate, BigDecimal totalAmount) { // No defensive copies needed for immutable types this.orderId = orderId; this.orderDate = orderDate; this.totalAmount = totalAmount; } // Getters can return directly — nothing to mutate public String getOrderId() { return orderId; } public LocalDate getOrderDate() { return orderDate; }} // EXAMPLE 2: Private helper method — document the expectationpublic class Calculator { /** * Processes data. Caller must not modify 'data' after calling. * (Private method — internal use only) */ private double processInternal(List<Double> data) { // Within the class, we trust ourselves not to misuse return data.stream().mapToDouble(d -> d).average().orElse(0); }} // EXAMPLE 3: Builder pattern — copy at build() onlypublic class PersonBuilder { private String name; private final List<String> nicknames = new ArrayList<>(); public PersonBuilder addNickname(String nickname) { nicknames.add(nickname); // Mutable during building return this; } public Person build() { // Defensive copy only at the final step return new Person(name, List.copyOf(nicknames)); }} // EXAMPLE 4: Internally created object returned directlypublic Order createOrder() { // We just created this ArrayList; no one else has a reference List<Item> items = new ArrayList<>(); items.add(fetchDefaultItem()); // It's safe to pass directly to Order's constructor if Order // trusts its callers (which here is just us) return new Order(items); // No copy needed of 'items'}If you're unsure whether to defensively copy, err on the side of copying. The performance cost is usually negligible, and the bugs you prevent are severe. Only skip copying when you can articulate clearly why it's safe.
Defensive copying isn't the only way to protect encapsulation. Several alternatives achieve similar safety with different tradeoffs.
The best defense is using types that can't be mutated. If a type is immutable, references can be freely shared without any copying.
Strategy: Replace mutable types with immutable equivalents:
Date → LocalDate, LocalDateTime, Instantjava.util.List → List.of(), Guava ImmutableListThis eliminates the problem at its source.
12345678910111213141516171819202122232425262728293031
// BEFORE: Defensive copying needed because Date is mutablepublic class Meeting { private final Date startTime; public Meeting(Date startTime) { this.startTime = new Date(startTime.getTime()); // Copy } public Date getStartTime() { return new Date(startTime.getTime()); // Copy }} // AFTER: No copying needed because Instant is immutablepublic class Meeting { private final Instant startTime; // Immutable! public Meeting(Instant startTime) { this.startTime = startTime; // No copy needed } public Instant getStartTime() { return startTime; // No copy needed }} // Using Java records for immutable value types (Java 16+)public record Point(int x, int y) {}public record Range(LocalDate start, LocalDate end) {} // No defensive copying needed for records with immutable fieldsUse this checklist when reviewing code or designing new classes. Walk through these questions in order:
12345678910111213141516171819202122232425262728293031323334
DEFENSIVE COPYING DECISION TREE Is the type immutable (String, LocalDate, primitives, immutable records)?├── YES → No defensive copy needed. DONE.└── NO → Continue... Is this a security-sensitive context (auth, permissions, crypto)?├── YES → ALWAYS defensively copy. DONE.└── NO → Continue... Is this a public API (public method, library interface)?├── YES → Defensive copy on both input and output. DONE.└── NO → Continue... Do you control all callers (private/internal method)?├── YES → Skip copying IF you document the expectation│ and trust your team to follow convention.└── NO → Defensive copy. Is the object stored as a collection key (HashMap, HashSet)?├── YES → Defensive copy to prevent key mutation. DONE.└── NO → Continue... Is the class advertised as immutable or thread-safe?├── YES → Defensive copy to honor the contract. DONE.└── NO → Continue... Is this performance-critical code with profiling showing copy overhead?├── NO → Default to defensive copying (it's usually cheap).└── YES → Consider alternatives: - Use immutable types - Use unmodifiable wrappers - Document ownership transfer (carefully!) - Accept the risk with thorough documentationLet's apply our decision framework to realistic scenarios.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
// SCENARIO 1: E-commerce Order classpublic class Order { private final String orderId; // Immutable → no copy private final BigDecimal totalPrice; // Immutable → no copy private final Instant createdAt; // Immutable → no copy private final List<OrderItem> items; // Mutable list → must protect private final Address shippingAddress; // Mutable → must protect public Order(String orderId, List<OrderItem> items, Address shippingAddress, BigDecimal totalPrice) { // String, BigDecimal, Instant: no copy needed this.orderId = Objects.requireNonNull(orderId); this.totalPrice = totalPrice; this.createdAt = Instant.now(); // List<OrderItem>: copy on input (public constructor) // If OrderItem is mutable, deep copy needed this.items = items.stream() .map(OrderItem::new) // Deep copy .collect(Collectors.toList()); // Address: copy on input this.shippingAddress = new Address(shippingAddress); } public List<OrderItem> getItems() { // Return immutable copy on output return items.stream() .map(OrderItem::new) .collect(Collectors.toUnmodifiableList()); } // String getter: no copy needed (immutable) public String getOrderId() { return orderId; }} // SCENARIO 2: Internal service (package-private)// Only called by trusted code within same package /*package*/ class OrderProcessor { // Package-private: called only by our OrderService // Skip input copy IF we document and trust our team void processItems(List<OrderItem> items) { // INTERNAL USE ONLY: Caller must not mutate 'items' after calling. // (Comment here + code review enforcement) for (OrderItem item : items) { // process... } }} // SCENARIO 3: Configuration object (read-heavy, rarely changes) public class AppConfig { private final Map<String, String> settings; private Map<String, String> cachedUnmodifiable; public AppConfig(Map<String, String> settings) { // Defensive copy on input this.settings = new HashMap<>(settings); } // This is called thousands of times; copying each time is wasteful // Use cached unmodifiable view instead public Map<String, String> getSettings() { if (cachedUnmodifiable == null) { cachedUnmodifiable = Collections.unmodifiableMap(settings); } return cachedUnmodifiable; // O(1), no allocation }} // SCENARIO 4: High-performance buffer (ownership transfer) /** * Zero-copy buffer for network I/O. * INTERNAL USE ONLY. *//*package*/ class NetworkBuffer { private byte[] data; /** * OWNERSHIP TRANSFER: Do not use 'data' after calling. */ NetworkBuffer(byte[] data) { // No copy for performance; ownership is transferred this.data = data; } // Only expose through controlled access byte readByte(int offset) { return data[offset]; }}Even experienced developers make these mistakes. Learn to recognize and avoid them.
During code review, ask: 'For each method that accepts or returns an object, could external code use that reference to corrupt internal state?' If yes and there's no defensive copy, it's a bug.
We've developed a complete framework for deciding when to defensively copy. Let's consolidate the key principles:
Module Complete:
You've now mastered defensive copying—understanding the mutable reference problem, implementing defensive copies on both input and output, and knowing when to apply or skip the technique. This knowledge is foundational for building robust, maintainable, secure object-oriented systems.
You now have the knowledge and decision framework to protect your objects from mutable reference attacks. You understand when defensive copying is essential, when alternatives suffice, and how to implement these techniques correctly. This completes the Defensive Copying module.