References
- Effective Java, "Item 13: Override the clone method prudently"
- Java in-depth understanding of shallow copy and deep copy | Nuggets
- Elaborate on deep copy and shallow copy of Java | Segmentfault
foreword
There are primitive types and reference types in Java. Assignment in Java is pass-by-value
- For basic types, the specific content is copied.
- For reference types, the stored value is only the address of the actual object, and copying will only copy the reference address.
On this basis, the "copy of the object" can be divided into two cases
- shallow copy
- Pass-by-value for primitive data types
- Make a copy of the reference address for the reference data type
- deep copy
- Pass-by-value for primitive data types
- For reference data types, create a new object and copy its contents
Copy related API
Object#clone
All objects in Java inherit from java.lang.Object
. Object
A method of protected
type clone
.
protected native Object clone()
throws CloneNotSupportedException;
复制代码
Object#clone()
The method is native
yes , so we don't need to implement it. It should be noted that the clone
method is protected
yes , which means that the clone
method is only visible in the java.lang
package or its subclasses.
This is not possible if we want to call a clone
method . Because the clone
methods are defined in Object
, the object has no externally visible clone
methods .
Cloneable interface
As mentioned above, Object#clone()
methods are protected
yes , we cannot directly call clone
methods on an object in a program.
The JDK recommends "implementing the Cloneable
interface and overriding the clone
method (using the public
modifier ) to implement a copy of the property".
package java.lang;
/**
* 此处给出 Cloneable 的部分注释
* A class implements the Cloneable interface to
* indicate to the java.lang.Object#clone() method that it
* is legal for that method to make a
* field-for-field copy of instances of that class.
*
* Invoking Object's clone method on an instance that does not implement the
* Cloneable interface results in the exception
* CloneNotSupportedException being thrown.
*
* By convention, classes that implement this interface should override
* Object.clone (which is protected) with a public method.
*/
public interface Cloneable {
}
复制代码
Reading the Cloneable
source code , there are the following conclusions
- 对于实现
Cloneable
接口的对象,是可以调用Object#clone()
方法来进行属性的拷贝。 - 若一个对象没有实现
Cloneable
接口,直接调用Object#clone()
方法,会抛出CloneNotSupportedException
异常。 Cloneable
是一个空接口,并不包含clone
方法。但是按照惯例(by convention
),实现Cloneable
接口时,应该以public
修饰符重写Object#clone()
方法(该方法在Object
中是被protected
修饰的)。
参照《Effective Java》中「第13条 谨慎地重写 clone 方法」
Cloneable
接口的目的是作为一个mixin
接口,约定如果一个类实现了Cloneable
接口,那么Object
的clone
方法将返回该对象的逐个属性(field-by-field
)拷贝(浅拷贝);否则会抛出CloneNotSupportedException
异常。
上面第1、2点,可用下面的伪代码描述。
protected Object clone throws CloneNotSupportedException {
if(!(this instanceof Cloneable)){
throw new CloneNotSupportedException("Class" + getClass().getName() + "doesn't implement Cloneable");
}
return internalClone();
}
/**
* Native Helper method for cloning.
*/
private native Object internalClone();
复制代码
浅拷贝
参考 Cloneable
接口的源码注释部分,如果一个类实现了 Cloneable
接口,那么 Object
的 clone
方法将返回该对象的逐个属性(field-by-field
)拷贝,这里的拷贝是浅拷贝。
- 对基本数据类型进行值传递
- 对引用数据类型进行引用地址的拷贝
下面结合一个示例加以说明。
- 定义两个对象,
Address
和CustomerUser
@Data
class Address implements Cloneable{
private String name;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data
class CustomerUser implements Cloneable{
private String firstName;
private String lastName;
private Address address;
private String[] cars;
@Override
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
复制代码
- 在测试方法
testShallowCopy
中,使用customerUser.clone()
进行对象拷贝。注意,此处为浅拷贝。
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
testShallowCopy();
}
public static void testShallowCopy() throws CloneNotSupportedException {
Address address= new Address();
address.setName("北京天安门");
CustomerUser customerUser = new CustomerUser();
customerUser.setAddress(address);
customerUser.setLastName("李");
customerUser.setFirstName("雷");
String[] cars = new String[]{"别克","路虎"};
customerUser.setCars(cars);
//浅拷贝
CustomerUser customerUserCopy =(CustomerUser) customerUser.clone();
customerUserCopy.setFirstName("梅梅");
customerUserCopy.setLastName("韩");
customerUserCopy.getAddress().setName("北京颐和园");
customerUserCopy.getCars()[0]="奥迪";
System.out.println("customerUser: " + JSONUtil.toJsonStr(customerUser));
System.out.println("customerUserCopy: " + JSONUtil.toJsonStr(customerUserCopy));
}
}
复制代码
- 程序运行结果如下。
customerUser: {"lastName":"李","address":{"name":"北京颐和园"},"firstName":"雷","cars":["奥迪","路虎"]}
customerUserCopy: {"lastName":"韩","address":{"name":"北京颐和园"},"firstName":"梅梅","cars":["奥迪","路虎"]}
复制代码
- 可以看到,修改拷贝之后的
customerUserCopy
的引用类型的属性值(Address
和String[]
类型的属性值),会影响到原对象customerUser
。
深拷贝
实现深拷贝有两种方式,「序列化对象方式」和「二次调用 clone
方式」
- 序列化(
serialization
)方式- 先对对象进行序列化,再进行反序列化,得到一个新的深拷贝的对象
- 二次调用
clone
方式- 先调用对象的
clone()
方法 - 对对象的引用类型的属性值,继续调用
clone()
方法进行拷贝
- 先调用对象的
下面,在「浅拷贝」章节示例的基础上,使用「二次调用 clone
方式」实现深拷贝。
- 修改
CustomerUser
的clone()
方法,对CustomerUser
对象的引用类型的属性值,即Address
属性值和数组(String[]
)属性值cars
,二次调用clone
方法。
@Data
class CustomerUser implements Cloneable{
private String firstName;
private String lastName;
private Address address;
private String[] cars;
@Override
public Object clone() throws CloneNotSupportedException{
CustomerUser customerUserDeepCopy = (CustomerUser) super.clone();
//二次调用clone方法
customerUserDeepCopy.address = (Address) address.clone();
customerUserDeepCopy.cars = cars.clone();
return customerUserDeepCopy;
}
}
复制代码
- 再次运行程序,输出结果如下。
customerUser: {"lastName":"李","address":{"name":"北京天安门"},"firstName":"雷","cars":["别克","路虎"]}
customerUserCopy: {"lastName":"韩","address":{"name":"北京颐和园"},"firstName":"梅梅","cars":["奥迪","路虎"]}
复制代码
- 可以看到
address
和cars
是不同的,表示我们的深拷贝是成功的。
创建对象
- ref 1-Java中的clone和new的效率对比
在介绍 clone
方法的基础上,引出对「创建对象的4种方法」,「clone和new的效率对比」等问题的介绍。
创建对象的4种方法
创建对象的 4 种方法如下
- 使用
new
关键字 - 反射机制
- 实现
Cloneable
接口,使用clone
方法创建对象 - 序列化和反序列化
以上 4 种方式,都可以创建一个 Java 对象,实现机制上有如下区别
- 方式 1 和方式 2 中,都明确地显式地调用了对象的构造函数。
- 方式 3 中,是对已经的对象,在内存上拷贝了一个影印,并不会调用对象的构造函数。
- 方式 4 中,对对象进行序列化,转化为了一个文件流,再通过反序列化生成一个对象,也不会调用构造函数。
clone和new的效率对比
- 使用
clone
创建对象,该操作并不会调用类的构造函数,是在内存中进行的数据块的拷贝,复制已有的对象。 - 使用
new
方式创建对象,调用了类的构造函数。
使用 clone
创建对象,直接在内存中进行数据块的拷贝。这是否意味着 clone
方法的效率更高呢?
答案并不是,JVM 的开发者意识到通过 new
方式来生成对象的方式,使用的更加普遍,所以对于利用 new
操作生成对象进行了优化。
下面编写一个测试用例,用 clone
和 new
两种方式来创建 10000 * 1000
个对象,测试对应的耗时。
public class Bean implements Cloneable {
private String name;
public Bean(String name) {
this.name = name;
}
@Override
protected Bean clone() throws CloneNotSupportedException {
return (Bean) super.clone();
}
}
public class TestClass {
private static final int COUNT = 10000 * 1000;
public static void main(String[] args) throws CloneNotSupportedException {
long s1 = System.currentTimeMillis();
for (int i = 0; i < COUNT; i++) {
Bean bean = new Bean("ylWang");
}
long s2 = System.currentTimeMillis();
Bean bean = new Bean("ylWang");
for (int i = 0; i < COUNT; i++) {
Bean b = bean.clone();
}
long s3 = System.currentTimeMillis();
System.out.println("new = " + (s2 - s1));
System.out.println("clone = " + (s3 - s2));
}
}
复制代码
程序输出如下。
new = 7
clone = 83
复制代码
可以看到,创建 10000 * 1000
个对象,使用 new
方法的耗时,只有 clone
方式的 1/10,即 new
方式创建对象的效率更高。
但是,若构造函数中有一些耗时操作,则 new
方式创建对象的效率会受到构造函数性能的影响。如下代码,在构造函数中添加字符串截取的耗时操作。
public class Bean implements Cloneable {
private String name;
private String firstSign;//获取名字首字母
public Bean(String name) {
this.name = name;
if (name.length() != 0) {
firstSign = name.substring(0, 1);
firstSign += "abc";
}
}
@Override
protected Bean clone() throws CloneNotSupportedException {
return (Bean) super.clone();
}
}
复制代码
此时,再执行测试用例,创建 10000 * 1000
个对象,程序输出如下,使用 new
方法的耗时,就远大于 clone
方式了。
new = 7
clone = 83
复制代码
Finally, a conclusion is drawn on the " efficiency comparison of andclone
"new
- The JVM optimizes the way objects are created using the
new
method , and by default,new
is more efficient. new
When an object is created by the method, the constructor of the class is called. If there is a time-consuming operation in the constructor, it will affect the efficiency of thenew
method to create the object.clone
method to create an object without calling the constructor of the class.
Based on the above conclusions, in the "Deep Copy" section above, the implementation of the deep copy function can be optimized, instead of calling the clone
method to create the object, instead directly calling the constructor to implement it.
@Data
class Address implements Cloneable{
private String name;
Address(Address address){
this.name=address.name;
}
}
@Data
class CustomerUser implements Cloneable{
private String firstName;
private String lastName;
private Address address;
private String[] cars;
CustUserDeep(CustUserDeep custUserDeep){
this.firstName = custUserDeep.firstName;
this.lastName = custUserDeep.lastName;
this.cars = custUserDeep.getCars().clone();
this.address = new Address(custUserDeep.getAddress());
}
}
复制代码