Referências
- Java efetivo, "Item 13: Substituir o método de clonagem com prudência"
- Compreensão profunda de Java de cópia superficial e cópia profunda | Nuggets
- Elaborar em cópia profunda e cópia superficial de Java | Segmentfault
prefácio
Existem tipos primitivos e tipos de referência em Java. A atribuição em Java é passada por valor
- Para tipos básicos, o conteúdo específico é copiado.
- Para tipos de referência, o valor armazenado é apenas o endereço do objeto real e a cópia copiará apenas o endereço de referência.
Com base nisso, a "cópia do objeto" pode ser dividida em dois casos
- cópia superficial
- Passagem por valor para tipos de dados primitivos
- Faça uma cópia do endereço de referência para o tipo de dados de referência
- cópia profunda
- Passagem por valor para tipos de dados primitivos
- Para tipos de dados de referência, crie um novo objeto e copie seu conteúdo
Copiar API relacionada
Objeto#clone
Todos os objetos em Java herdam de java.lang.Object
. Object
Um método de protected
tipo clone
.
protected native Object clone()
throws CloneNotSupportedException;
复制代码
Object#clone()
O método é native
yes , então não precisamos implementá-lo. Deve-se notar que o clone
método é protected
yes , o que significa que o clone
método só é visível no java.lang
pacote ou em suas subclasses.
Isso não é possível se quisermos chamar um clone
método . Como os clone
métodos são definidos em Object
, o objeto não possui clone
métodos .
Interface clonável
Como mencionado acima, Object#clone()
os métodos são protected
yes , não podemos chamar diretamente clone
métodos em um objeto em um programa.
O JDK recomenda "implementar a Cloneable
interface e sobrescrever o clone
método (usando o public
modificador ) para implementar uma cópia da propriedade".
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 {
}
复制代码
Lendo o Cloneable
código fonte , há as seguintes conclusões
- 对于实现
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
复制代码
Por fim, é feita uma conclusão sobre a " comparação de eficiência de eclone
"new
- A JVM otimiza a forma como os objetos são criados usando o
new
método e, por padrão,new
é mais eficiente. new
Quando um objeto é criado pelo método, o construtor da classe é chamado. Se houver uma operação demorada no construtor, isso afetará a eficiência donew
método para criar o objeto.clone
método para criar um objeto sem chamar o construtor da classe.
Com base nas conclusões acima, na seção "Deep Copy" acima, a implementação da função de cópia profunda pode ser otimizada, em vez de chamar o clone
método para criar o objeto, em vez de chamar diretamente o construtor para implementá-lo.
@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());
}
}
复制代码