Defensive Copying: Preserving Object Integrity
When Defensive Copying Is Necessary
Read aloud is unavailable in this browser.
The Art of Knowing When
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 Trust Boundary Concept
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.
Situations That Always Require Defensive Copying
Certain situations demand defensive copying regardless of current callers or performance concerns. These are non-negotiable for professional code.
- •Public API with mutable parameters or returns. Any public method that accepts or returns mutable objects. Public APIs are called from code you don't control—today's internal API often becomes tomorrow's public library.
- •Security-sensitive data. Anything related to authentication, authorization, permissions, credentials, or cryptographic material. The cost of a security breach vastly outweighs any copying overhead.
- •Data crossing process/network boundaries. When serializing/deserializing data or receiving data from external services. You have zero control over the source.
- •Immutability is a documented guarantee. If your class's contract promises immutability (even implicitly), defensive copying is required to honor that promise.
- •Objects stored in collections serving as keys. Objects used as HashMap keys or HashSet elements must not change after insertion. Defensive copy to prevent mutation that would corrupt the collection.
- •Multi-threaded access without synchronization. If objects might be accessed from multiple threads and you're not using synchronization, immutability (via defensive copying) is your safety mechanism.
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); }}Situations Where Defensive Copying Can Be Skipped
Not every method needs defensive copying. Over-copying wastes resources and obscures intent. Here's when you can safely skip it.
- •Objects are already immutable. String, LocalDate, LocalDateTime, Integer, BigDecimal, record types with immutable fields—these cannot be mutated, so copying is pointless.
- •Private methods within the same class. You control both caller and callee; if you trust yourself not to mutate, copying is redundant. But document the expectation!
- •Package-private methods in cohesive packages. If the entire package is under your control and follows consistent conventions, you can skip copying at internal boundaries.
- •Explicit ownership transfer. Some APIs are designed so that passing an object transfers ownership—the caller promises not to use it afterward. Document this clearly.
- •Builder pattern internals. During construction, builders often share mutable state and copy only at the final build() step.
- •Value objects you create internally. When you create a mutable object inside a method and return it directly, no one else has a reference to copy from.
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.
Alternatives to Defensive Copying
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(), GuavaImmutableList- Custom mutable classes → Records, immutable value objects
This 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 fieldsPractical Decision Checklist
Use 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 documentation- •Public methods: Copy
- •Private methods in same class: Usually skip, document expectations
- •Methods accepting mutable parameters: Copy on input
- •Methods returning mutable internals: Copy on output (or return unmodifiable)
- •Security data: Always copy
- •Immutable types: Never copy
- •Collection keys: Copy before storing
Real-World Decision Examples
Let'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]; }}Common Mistakes and How to Avoid Them
Even experienced developers make these mistakes. Learn to recognize and avoid them.
- •Copying only on input OR output. Both attack vectors must be closed. A class with defensive input copying but naked getters is still vulnerable.
- •Shallow copy of mutable elements. Copying a List<Date> creates a new list pointing to the same Date objects. If elements are mutable, deep copy is required.
- •Using clone() for untrusted input. Clone can be overridden by malicious subclasses. Use copy constructors for input parameters.
- •Not considering future callers. 'My code never misuses this' doesn't protect against future developers. Public APIs should be self-protecting.
- •Over-copying immutable types. Creating new String or LocalDate copies wastes resources. Know which types are immutable.
- •Forgetting arrays. Arrays have no unmodifiable wrapper. Every array return must be a copy.
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.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.