Shallow and Deep Java Cloning
While not recommended, cloning is a viable way to copy an object. Let's dive into Shallow Cloning, Deep Cloning, how to use them both, and when to avoid them.
Join the DZone community and get the full member experience.
Join For FreeIn my previous article, 5 Different ways to create objects in Java, I discussed five different ways (new
keyword, Class.newInstance() method, Constructor.newInstance() method, cloning and serialization-deserialization) a developer can create objects. If you haven't read it, please go ahead.
Cloning is also a way of creating an object, but in general, cloning is not just about creating a new object. Cloning means creating a new object from an already present object and copying all data of the given object to that new object.
In order to create a clone of an object, we generally design our class in such a way that:
- Our class should implement the Cloneable interface. Otherwise, the JVM will throw CloneNotSupportedException if we will call clone() on our object.
- Our class should have a clone method, which should handle CloneNotSupportedException.
It is not necessary to define our method by the name of clone. We can give it any name we want, e.g. createCopy(). Actually we are not overriding the Object.clone() method here, so we don’t have to follow any specification. Object.clone() is protected by its definition, so, practically, child classes of Object outside the package of the Object class (java.lang) can only access it through inheritance and within itself. For details on the protected access specifier, please read Why an outer Java class can’t be private or protected .
- And finally, we need to call the clone() method of the superclass, which will call it's super’s clone(). This chain will continue until it reaches the clone() method of the Object class. The Object.clone() method is the actual worker that creates the clone of your object, and another clone() method just delegates the call to its parent’s clone().
These steps lead to a disadvantage of the cloning strategy to copy objects because all of above steps are necessary to make cloning work. But we can choose to not do all the above steps by following other strategies e.g. Copy Constructors. To know more read Java Cloning - Copy Constructor versus Cloning.
To demonstrate cloning, we will create two classes — Person and Cit. Both classes have:
- toString() to show the content of the person object.
- equals() and hashCode() methods to compare the objects.
- clone() to clone the object.
And as we can see, toString(), equals(), and hashCode() have the @Override annotation, while the clone() method doesn’t have it because we are not overriding it from clone — we are creating a new method by the name of clone(), and we can name it anything else.
class Person implements Cloneable {
private String name;
private int income;
private City city;
public String getName() {
return name;
}
public void setName(String firstName) {
this.name = firstName;
}
public int getIncome() {
return income;
}
public void setIncome(int income) {
this.income = income;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
public Person(String firstName, int income, City city) {
super();
this.name = firstName;
this.income = income;
this.city = city;
}
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
@Override
public String toString() {
return "Person [name=" + name + ", income=" + income + ", city=" + city + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((city == null) ? 0 : city.hashCode());
result = prime * result + income;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (city == null) {
if (other.city != null)
return false;
} else if (!city.equals(other.city))
return false;
if (income != other.income)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
The Person class has a reference to the City class, which looks like below:
class City implements Cloneable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public City(String name) {
super();
this.name = name;
}
public City clone() throws CloneNotSupportedException {
return (City) super.clone();
}
@Override
public String toString() {
return "City [name=" + name + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
City other = (City) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
And let's test it:
public class CloningExample {
public static void main(String[] args) throws CloneNotSupportedException {
City city = new City("Dehradun");
Person person1 = new Person("Naresh", 10000, city);
System.out.println(person1);
Person person2 = person1.clone();
System.out.println(person2);
if (person1 == person2) {
System.out.println("Both person1 and person2 holds same object");
}
if (person1.equals(person2)) {
System.out.println("But both person1 and person2 are equal and have same content");
}
if (person1.getCity() == person2.getCity()) {
System.out.println("Both person1 and person2 have same city object");
}
}
}
person1.clone() calls super.clone(), which means the Object.clone() method.
The Object.clone() method copies the content of the object to another object bit-by-bit, meaning the values of all instance variables from one object will get copied to instance variables of other objects.
So (person1 == person2) will evaluate false because person1 and person2 are copies of each other, but both are different objects and hold different spots in heap memory. Meanwhile, person1.equals(person2) evaluates true because both have the same content.
But as we know, reference variables hold the address of the object instead of object itself, which can also be referred from other reference variables. And if we change one, the other will reflect that change.
So Object.clone() will copy the address that person1.city is holding to person2.city, so now city, person1.city, and person2.city all are holding the same city object. That’s why (person1.getCity() == person2.getCity()) evaluates true. This behaviour of cloning is known as Shallow Cloning
.
Types of Cloning
The behavior of the Object.clone() method classifies cloning into two sections.
1. Shallow Cloning
This is the default cloning strategy provided by Object.clone(), which we have seen. The clone() method of the Object class creates a new instance and copies all fields of the Cloneable object to that new instance (either it is primitive or a reference). So in the case of reference types, only reference bits get copied to the new instance. Therefore, the reference variable of both objects will point to the same object. The example we have seen above is an example of Shallow Cloning.
2. Deep Cloning
As the name suggests, deep cloning means copying everything from one object to another object. To achieve this, we will need to trick our clone() method provide our own cloning strategy. We can do it by implementing a Cloneable interface and overriding the clone() method in every reference type we have in our object hierarchy. Then, we call super.clone() and these clone() methods in our object’s clone method.
So we can change the clone method of the Person class in the following way:
public Person clone() throws CloneNotSupportedException {
Person clonedObj = (Person) super.clone();
clonedObj.city = this.city.clone();
return clonedObj;
}
Now (person1.getCity() == person2.getCity()) will evaluate false because, in the clone() method of the Person class, we are cloning the city object and assigning it to the new cloned person object.
In the example below, we have deep-copied the city object by implementing clone() in the City class and calling that clone() method of a person class, That's why person1.getCity() == person2.getCity() evaluates false — because both are separate objects. But we have not done the same with the Country class, person1.getCountry() == person2.getCountry() evaluates true.
public class CloningExample {
public static void main(String[] args) throws CloneNotSupportedException {
City city = new City("Dehradun");
Country country = new Country("India");
Person person1 = new Person("Naresh", 10000, city, country);
System.out.println(person1);
Person person2 = person1.clone();
System.out.println(person2);
if (person1 == person2) {
System.out.println("Both person1 and person2 holds same object");
}
if (person1.equals(person2)) {
System.out.println("But both person1 and person2 are equal and have same content");
}
if (person1.getCity() == person2.getCity()) {
System.out.println("Both person1 and person2 have same city object");
}
if (person1.getCountry() == person2.getCountry()) {
System.out.println("Both person1 and person2 have same country object");
}
city.setName("Pune");
country.setName("IN");
System.out.println(person1);
System.out.println(person2);
}
}
class Person implements Cloneable {
private String name;
private int income;
private City city;
private Country country;
public String getName() {
return name;
}
public void setName(String firstName) {
this.name = firstName;
}
public int getIncome() {
return income;
}
public void setIncome(int income) {
this.income = income;
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
public Country getCountry() {
return country;
}
public void setCountry(Country country) {
this.country = country;
}
public Person(String name, int income, City city, Country country) {
super();
this.name = name;
this.income = income;
this.city = city;
this.country = country;
}
public Person clone() throws CloneNotSupportedException {
Person clonedObj = (Person) super.clone();
clonedObj.city = this.city.clone();
return clonedObj;
}
@Override
public String toString() {
return "Person [name=" + name + ", income=" + income + ", city=" + city + ", country=" + country + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((city == null) ? 0 : city.hashCode());
result = prime * result + ((country == null) ? 0 : country.hashCode());
result = prime * result + income;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (city == null) {
if (other.city != null)
return false;
} else if (!city.equals(other.city))
return false;
if (country == null) {
if (other.country != null)
return false;
} else if (!country.equals(other.country))
return false;
if (income != other.income)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
class City implements Cloneable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public City(String name) {
super();
this.name = name;
}
public City clone() throws CloneNotSupportedException {
return (City) super.clone();
}
@Override
public String toString() {
return "City [name=" + name + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
City other = (City) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
class Country {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Country(String name) {
super();
this.name = name;
}
@Override
public String toString() {
return "Country [name=" + name + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Country other = (Country) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
Conclusion
Despite everything you've learned here, Java cloning is not considered a good way to copy an object, and there are lots of other ways to do the same. For instance, you can read Java Cloning — Copy Constructor versus Cloning,
to get more knowledge on why Java cloning is not preferred and what you can do instead.
Published at DZone with permission of Naresh Joshi, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments