When working with Java objects, it’s crucial to understand how objects can be copied. Objects can be copied either shallowly or deeply, and grasping these concepts is essential when you need to duplicate an object’s state without affecting the original object.
In this article, we’ll explore various methods of creating deep and shallow copies of objects in Java, including some optimized approaches and potential pitfalls to avoid.
Shallow Copy
A shallow copy creates a new object but references the same elements as the original object. In other words, the new object contains copies of the values of the original object’s fields, but any references to other objects remain the same.
Here’s an example of creating a shallow copy in Java:
| |
Now let’s see how shallow copying works:
| |
In this example, when we assign person1 to person2, we’re creating a shallow copy. As a result, both person1 and person2 refer to the same Person object in memory. Modifying the phoneNumbers property of person2 also affects the phoneNumbers property of person1.
Shallow Copy Methods
There are several ways to create shallow copies in Java:
Object.clone() method: Requires implementing the
Cloneableinterface.1 2 3 4 5 6 7 8public class Person implements Cloneable { // ... fields and other methods ... @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } }Copy Constructor: Can be used for shallow copying if it only copies references.
1 2 3 4public Person(Person other) { this.name = other.name; // Shallow copy of reference this.age = other.age; }
Does Using Clone() make a Deep Copy?
The clone() method typically creates a shallow copy, not a deep copy. This means that while it creates a new object, it doesn’t create new instances of the mutable objects contained within the original object.
Here’s an example using the Cloneable interface:
| |
Now let’s look at a test to understand it better:
| |
In this example, we use the clone() method to create a shallow copy of the PersonCloneable object. This creates a new PersonCloneable object with its own memory location, independent of the original person1 object.
However, modifying the phoneNumbers property of the clone affects the phoneNumbers of the original. That’s one of the potential issues of using clone(), as it makes shallow copies, not deep copies.
Potential Issues with Clone()
While the clone() method can be used to create copies of objects, it has some significant issues:
Needs Cloneable interface: To use the
clone()method, a class must explicitly implement theCloneableinterface. By default, theclone()method is protected in theObjectclass, and if a class doesn’t implement it, trying to useclone()will result in aCloneNotSupportedException.Default Shallow Copy: The default
clone()method in theObjectclass creates a shallow copy. This means that if the object contains references to other mutable objects, the cloned object will still share those references with the original, rather than duplicating the referenced objects themselves.Security Concerns: Overriding the
clone()method allows for custom cloning behavior, but it also introduces potential security risks. Here’s a more detailed explanation:
Imagine you’re at a copy machine in an office. You’re supposed to make an exact copy of a document, but someone has tampered with the machine. Instead of making a true copy, it might add or remove pages, or even worse, send a copy to someone else! That’s similar to what can happen with the clone() method if it’s not implemented correctly.
Here’s why:
Unexpected Behavior: When you override
clone(), you’re in charge of deciding exactly how the copying happens. If you’re not careful, you might accidentally expose private data or create inconsistent objects.The “Fake Copy” Problem: A malicious subclass could override
clone()to return a reference to itself instead of creating a new object. This could trick other parts of the code into thinking they have a fresh copy when they don’t.Bypassing Security Checks: Normally, when you create an object, its constructor runs and can perform important security checks. But
clone()bypasses the constructor, potentially creating objects in an insecure state.Inconsistent State: If
clone()is interrupted (like by an exception), it might leave the new object half-created. This could lead to bugs or vulnerabilities if the code isn’t prepared to handle partially-cloned objects.
Here’s a simple example to illustrate:
| |
In this example, a programmer using the SecretKeeper class might think they’re safely changing only the cloned object’s password, but they’re actually changing the original object’s password too. This could lead to serious security issues in a real application.
To avoid these pitfalls, many Java developers prefer to use copy constructors or factory methods for object copying. These approaches give you more control and make it easier to ensure that you’re creating secure, consistent copies of objects.
Deep Copy Methods
To perform a deep copy in Java, we have multiple options such as using a custom copy method or a copy constructor. Furthermore, we can leverage serialization and third-party libraries for this purpose. Below, we’ll discuss deep copy methods including custom copy methods and serialization.
Custom Copy Method
One way to create a deep copy is to manually create new instances of all mutable objects. Here’s an example:
| |
Serialization
Java serialization is one of the most efficient ways of creating deep copies of an object. This approach works well for serializable objects (those that implement the Serializable interface). Using third-party libraries like Jackson and Gson, you can also do this.
For example, let’s modify the Person class to make it serializable:
| |
Now our custom deepCopy() method will use serialization-deserialization to copy an object:
| |
Here we are making deep copies of an object by serializing it into a byte array and then deserializing it from that byte array. This approach works well for creating a deep copy but requires that all objects in the graph are Serializable.
Now let’s see its use:
| |
This method serializes the object to a byte array and then deserializes it to create a new instance. From the test, we can see that it can effectively create a deep copy of the PersonSerializable object.
Other Deep Copy Methods
JSON Serialization/Deserialization: Use libraries like Jackson or Gson.
1 2ObjectMapper mapper = new ObjectMapper(); Person copy = mapper.readValue(mapper.writeValueAsString(original), Person.class);Cloning Libraries: Use third-party libraries like Apache Commons Lang.
1Person copy = SerializationUtils.clone(original);Using Reflection:
Another powerful technique for creating deep copies is using reflection. This method allows you to create a deep copy of an object without the need for implementing Cloneable or writing custom constructors. It works by recursively copying the fields of an object, even if the fields are private or inaccessible.
Here’s an example of how to implement deep copying using reflection:
| |
This method works by:
- Creating a new instance of the object’s class.
- Iterating through all declared fields of the object.
- For each field, if it’s not a primitive, String, or Number (which are immutable), it recursively applies the deep copy method.
Otherwise, it simply copies the value. Here the method might produce cyclic dependencies which can be resolved by checking if we already created the object. Thats why we have a
visitedmap in the example to resolve that.
Let’s see how we can use this method:
| |
This reflection-based deep copying technique has several advantages and disadvantages:
Pros:
It works without modifying your class or implementing interfaces. It can copy private and protected fields. It’s a generic solution that can work with any object.
Cons:
This approach can be slower due to the use of reflection. It might require handling some special cases, such as cyclic dependencies. It may not work correctly with final fields or certain complex objects.
When using this method, be aware of its limitations and test thoroughly with your specific object structures. Also, consider the performance implications, especially when dealing with large object graphs or in performance-critical sections of your code.
Optimized Approaches
Copy-on-Write: For collections, Java provides copy-on-write implementations like
CopyOnWriteArrayList.1 2 3 4 5CopyOnWriteArrayList<Person> list = new CopyOnWriteArrayList<>(); // Adding elements doesn't create a copy list.add(new Person("John", 30)); // But modifying operations do list.set(0, new Person("Jane", 28));Immutable Objects: If objects are immutable, shallow copying is effectively the same as deep copying.
1 2 3 4 5 6 7 8 9 10 11public final class ImmutablePerson { private final String name; private final int age; public ImmutablePerson(String name, int age) { this.name = name; this.age = age; } // Only getters, no setters }Lazy Copying: Implement a copy-on-write mechanism for individual fields.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32public class LazyPerson { private String name; private int age; private LazyPerson original; public LazyPerson(String name, int age) { this.name = name; this.age = age; } public LazyPerson(LazyPerson other) { this.original = other; } public String getName() { if (original != null) { return original.getName(); } return name; } public void setName(String name) { if (original != null) { this.name = original.name; this.age = original.age; original = null; } this.name = name; } // Similar for age }
Conclusion
In this article, we’ve covered what shallow and deep copy means in Java. Shallow copies create new objects but maintain references to the same nested objects, while deep copies create entirely new objects with independent states, including nested objects.
We’ve explored various methods for creating both shallow and deep copies, including the clone() method, custom copy methods, serialization, and some optimized approaches. We’ve also discussed the potential security issues with using clone() and why many developers prefer alternative methods.
When choosing a copying method, consider the specific requirements of your application, including performance needs, object complexity, and maintenance considerations. Always profile your application to determine the most efficient method for your specific scenario.
Remember, the “best” approach often depends on the specific requirements of your application. Shallow copying might be sufficient for simple objects or when performance is critical, while deep copying is necessary when you need to create truly independent copies of complex object graphs.