Java Object Assignment: Reference vs Value Semantics
java object assignment: Understand how Java object assignment works: references, aliasing, and how to safely copy objects to avoid unintended shared state.
When you write B b = a; in Java, you are not copying the object that a points to. You are copying the reference stored in a, so both variables now point to the same object in memory. This is the fundamental rule of java object assignment, and it has deep consequences for how shared state, method arguments, and collections behave in real applications.
Consider the simplest case:
public class Point { public int x; public int y; public Point(int x, int y) { this.x = x; this.y = y; } } Point p1 = new Point(1, 2); Point p2 = p1; p2.x = 10; System.out.println(p1.x); // 10
Because p1 and p2 reference the same Point instance, mutating through p2 is visible through p1. This behavior is often surprising to developers coming from languages where assignment copies the value, but it is exactly how Java treats all non-primitive types.
The Reference Assignment Rule
Java has two categories of types: primitives and reference types. Primitives (int, boolean, double, etc.) store their values directly in the variable. Reference types (classes, arrays, interfaces, enums) store an address that points to the actual object on the heap.
int a = 5; int b = a; b = 7; System.out.println(a); // 5, unaffected int[] arr1 = {1, 2, 3}; int[] arr2 = arr1; arr2[0] = 99; System.out.println(arr1[0]); // 99, same array
The assignment arr2 = arr1 copies the reference, not the array contents. This is why modifying arr2 changes arr1. The same rule applies to every object type, including String (which is immutable, but the reference still points to the same instance).
Aliasing and Shared Mutable State
When two or more variables refer to the same mutable object, they are said to be aliases. Aliasing is not inherently wrong, but it becomes a problem when one part of the code mutates the object and another part does not expect that change. This is a common source of subtle bugs in large codebases.
class Order { private List<String> items = new ArrayList<>(); public List<String> getItems() { return items; // returns the internal reference } } Order order = new Order(); List<String> external = order.getItems(); external.add("unauthorized item"); // modifies internal state
Here the caller gained direct access to the Order's internal list. Any change made through external is reflected inside order. This is an aliasing leak. To prevent it, you must return a copy or an unmodifiable view, as discussed later.
Aliasing is not always bad. In some designs, sharing a common configuration object across multiple components is intentional and efficient. The key is to control who can mutate the shared object and to document that behavior clearly.
How Method Parameters Receive Object References
Java passes arguments to methods by value, but for reference types, the value being passed is the reference itself. This means the method receives a copy of the reference, not a copy of the object. The method can mutate the object's fields, but reassigning the parameter does not affect the caller's variable.
public static void changePoint(Point p) { p.x = 100; // mutates the object p = new Point(0, 0); // reassigns local reference, caller unaffected } Point original = new Point(1, 2); changePoint(original); System.out.println(original.x); // 100, not 0
The method changed original.x because p and original pointed to the same object. But the reassignment p = new Point(0, 0) only changed the local reference p; the caller's original still points to the original object.
This behavior is often summarized as "pass by value, but the value is a reference." Understanding this distinction is essential for designing methods that either mutate their arguments or return new objects.
Copying Objects Without Sharing State
When you need an independent copy of an object, you must explicitly create one. Java does not provide a default copy operation for arbitrary objects. The three common approaches are:
- Copy constructor: a constructor that takes an instance of the same type and copies its fields.
- Clone method: the
clone()method fromObject, but it requires implementingCloneableand has subtle pitfalls. - Static factory or builder: a method that creates a new instance from an existing one.
A copy constructor is straightforward and type-safe:
public class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public Point(Point other) { this(other.x, other.y); } // getters omitted } Point p1 = new Point(1, 2); Point p2 = new Point(p1); p2.x = 10; // won't compile if fields are final, but if mutable, p1 remains unchanged
If the object contains mutable fields, a shallow copy is insufficient. For example, copying a List field with a copy constructor that simply assigns the same list reference still shares the list. You need a deep copy that duplicates the contained objects as well.
public class Order { private final List<Item> items; public Order(Order other) { this.items = new ArrayList<>(other.items); // shallow copy of list, but Item objects are shared } }
To fully decouple the two Order instances, you must copy each Item as well. Deep copying is more expensive and requires knowing the internal structure of every referenced object.
Using Immutable Objects to Avoid Aliasing Problems
Immutability eliminates the entire class of bugs caused by shared mutable state. If an object's fields are final and the object is designed so that no method can modify its state after construction, then aliasing is harmless: every reference sees the same immutable data, and no one can change it.
public final class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } public Point withX(int newX) { return new Point(newX, this.y); } }
Now assigning Point p2 = p1; is safe because p2 cannot mutate the object. If you need a different value, you create a new instance. This is how String works, and it is why passing strings around is rarely a source of aliasing bugs.
Immutability is not always feasible. Some objects are naturally mutable, such as collections or builder objects. In those cases, you must either copy defensively or restrict access to the internal references.
Memory and Performance Implications of Object Assignment
Object assignment itself is cheap: it copies a reference, which is typically a single machine word. The cost comes from what you do with the reference. Copying an object, on the other hand, allocates new memory and may require copying nested structures. Deep copies can be significantly more expensive than simple reference assignment.
Consider a large object graph. If you assign references, you avoid copying and save memory, but you also share state. If you copy, you pay for allocation and CPU time, but you gain independence. The right choice depends on how the object will be used.
// Reference assignment: O(1) time, no new allocation List<String> shared = new ArrayList<>(); List<String> alias = shared; // Shallow copy: O(n) time, new list allocation, but elements shared List<String> shallowCopy = new ArrayList<>(shared); // Deep copy for immutable Strings: elements are immutable, shallow copy is effectively deep
For immutable elements, a shallow copy is safe and often sufficient. For mutable elements, you need a deep copy, which can be costly. This tradeoff is a common source of performance issues in applications that copy large object graphs unnecessarily.
Another subtlety is the garbage collector. When you create many copies, you increase allocation pressure, which can lead to more frequent GC cycles. Reusing references avoids that overhead, but only if you can manage the shared state safely.
Choosing a Copy Strategy for Your Use Case
There is no universal answer to whether you should assign references or copy objects. The decision depends on the ownership model and the expected mutation pattern.
Use reference assignment when:
- The object is immutable, so sharing is always safe.
- The object is a large, expensive-to-copy graph that is read-only in practice.
- The object is intentionally shared as a singleton or configuration holder.
Use a shallow copy when:
- The object has only primitive or immutable fields.
- You need a new container but the contained elements are immutable.
Use a deep copy when:
- The object contains mutable nested structures that will be modified independently.
- You are returning an internal collection from a getter and want to prevent caller modifications.
A common practical pattern is defensive copying: when you accept an object in a constructor or setter, copy it to avoid the caller retaining a reference to the internal state. Similarly, when you return an internal collection, return a copy or an unmodifiable view.
public class Order { private final List<Item> items; public Order(List<Item> items) { this.items = new ArrayList<>(items); // defensive copy } public List<Item> getItems() { return new ArrayList<>(items); // defensive copy on return } }
This approach ensures that the Order class fully owns its internal list. The cost is a copy on every access, which is acceptable for most business logic but may be too expensive for performance-critical paths. In those cases, you can return an unmodifiable view using Collections.unmodifiableList and document that the caller must not modify the underlying list.
Understanding java object assignment is not just a theoretical exercise. It directly affects how you design APIs, manage memory, and avoid concurrency issues. When multiple threads share a mutable object, the aliasing problem becomes a thread-safety problem. Immutable objects or defensive copies are often the simplest way to make concurrent code safe without locks.
In practice, the most maintainable approach is to prefer immutable objects wherever possible and to copy only when you need to isolate state. When you do copy, choose the shallowest copy that satisfies your requirements, and document the ownership semantics of every method that returns an object or accepts one as a parameter.