博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
java对象-clone()方法-原理和实现
阅读量:4283 次
发布时间:2019-05-27

本文共 47001 字,大约阅读时间需要 156 分钟。

java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的:

@Test      
public
void
testassign
(){
Person p1=
new Person();
p1.setAge(
31);
p1.setName(
"Peter");
Person p2=p1;
System.out.println(p1==p2);
//true
}

如果创建一个对象的新的副本,也就是说他们的初始状态完全一样,但以后可以改变各自的状态,而互不影响,就需要用到java中对象的复制,如原生的clone()方法。

如何进行对象克隆

Object对象有个clone()方法,实现了对象中各个属性的复制,但它的可见范围是protected的,所以实体类使用克隆的前提是:

① 实现Cloneable接口,这是一个标记接口,自身没有方法。 

② 覆盖clone()方法,可见性提升为public。

@Data      
public
class Person implements Cloneable {
private String name;
private Integer age;
private Address address;
@Override
protected
Object
clone
()
throws
CloneNotSupportedException {
return
super.clone();
}
}
@Test
public
void
testShallowCopy
()
throws
Exception{
Person p1=
new Person();
p1.setAge(
31);
p1.setName(
"Peter");
Person p2=(Person) p1.clone();
System.out.println(p1==p2);
//false
p2.setName(
"Jacky");
System.out.println(
"p1="+p1);
//p1=Person [name=Peter, age=31]
System.out.println(
"p2="+p2);
//p2=Person [name=Jacky, age=31]
}

该测试用例只有两个基本类型的成员,测试达到目的了。

事情貌似没有这么简单,为Person增加一个Address类的成员:

@Data      
public
class Address {
private String type;
private String value;
}

再来测试,问题来了。

@Test      
public
void
testShallowCopy
()
throws
Exception{
Address address=
new Address();
address.setType(
"Home");
address.setValue(
"北京");
Person p1=
new Person();
p1.setAge(
31);
p1.setName(
"Peter");
p1.setAddress(address);
Person p2=(Person) p1.clone();
System.out.println(p1==p2);
//false
p2.getAddress().setType(
"Office");
System.out.println(
"p1="+p1);
System.out.println(
"p2="+p2);
}

查看输出:

false      
p1=Person(
name=Peter, age=
31, address=Address(
type=Office,
value=北京))
p2=Person(
name=Peter, age=
31, address=Address(
type=Office,
value=北京))

遇到了点麻烦,只修改了p2的地址类型,两个地址类型都变成了Office。

浅拷贝和深拷贝

前面实例中是浅拷贝和深拷贝的典型用例。

浅拷贝:被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象。

深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象。

也就是说,一个默认的clone()方法实现机制,仍然是赋值。

如果一个被复制的属性都是基本类型,那么只需要实现当前类的cloneable机制就可以了,此为浅拷贝。

如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现cloneable接口并覆盖clone()方法。

@Data      
public
class Address implements Cloneable {
private String type;
private String value;
@Override
protected
Object
clone
()
throws
CloneNotSupportedException {
return
super.clone();
}
}

这样还不够,Person的clone()需要显式地clone其引用成员。

@Data      
public
class Person implements Cloneable {
private String name;
private Integer age;
private Address address;
@Override
protected
Object
clone
()
throws
CloneNotSupportedException {
Object obj=
super.clone();
Address a=((Person)obj).getAddress();
((Person)obj).setAddress((Address) a.clone());
return obj;
}
}

重新跑前面的测试用例:

false      
p1=Person(
name=Peter, age=
31, address=Address(
type=Home,
value=北京))
p2=Person(
name=Peter, age=
31, address=Address(
type=Office,
value=北京))

clone方式深拷贝小结

① 如果有一个非原生成员,如自定义对象的成员,那么就需要:

  • 该成员实现Cloneable接口并覆盖clone()方法,不要忘记提升为public可见。
  • 同时,修改被复制类的clone()方法,增加成员的克隆逻辑。

② 如果被复制对象不是直接继承Object,中间还有其它继承层次,每一层super类都需要实现Cloneable接口并覆盖clone()方法。

与对象成员不同,继承关系中的clone不需要被复制类的clone()做多余的工作。

一句话来说,如果实现完整的深拷贝,需要被复制对象的继承链、引用链上的每一个对象都实现克隆机制。

前面的实例还可以接受,如果有N个对象成员,有M层继承关系,就会很麻烦。

利用序列化实现深拷贝

clone机制不是强类型的限制,比如实现了Cloneable并没有强制继承链上的对象也实现;也没有强制要求覆盖clone()方法。因此编码过程中比较容易忽略其中一个环节,对于复杂的项目排查就是困难了。

要寻找可靠的,简单的方法,序列化就是一种途径。

  • 被复制对象的继承链、引用链上的每一个对象都实现java.io.Serializable接口。这个比较简单,不需要实现任何方法,serialVersionID的要求不强制,对深拷贝来说没毛病。

  • 实现自己的deepClone方法,将this写入流,再读出来。俗称:冷冻-解冻。

@Data      
public
class Person implements Serializable {
private String name;
private Integer age;
private Address address;
public
Person
deepClone
() {
Person p2=
null;
Person p1=
this;
PipedOutputStream out=
new PipedOutputStream();
PipedInputStream in=
new PipedInputStream();
try {
in.connect(out);
}
catch (IOException e) {
e.printStackTrace();
}
try(ObjectOutputStream bo=
new ObjectOutputStream(out);
ObjectInputStream bi=
new ObjectInputStream(in);) {
bo.writeObject(p1);
p2=(Person) bi.readObject();
}
catch (Exception e) {
e.printStackTrace();
}
return p2;
}
}

原型工厂类

为了便于测试,也节省篇幅,封装一个工厂类。

公平起见,避免某些工具库使用缓存机制,使用原型方式工厂。

public        
class PersonFactory{
public
static
Person
newPrototypeInstance
(){
Address address =
new Address();
address.setType(
"Home");
address.setValue(
"北京");
Person p1 =
new Person();
p1.setAddress(address);
p1.setAge(
31);
p1.setName(
"Peter");
return p1;
}
}

利用Dozer拷贝对象

Dozer是一个Bean处理类库。

maven依赖

net.sf.dozer
dozer
5.5.1

测试用例:

@Data      
public
class Person {
private String name;
private Integer age;
private Address address;
@Test
public
void
testDozer
() {
Person p1=PersonFactory.newPrototypeInstance();
Mapper mapper =
new DozerBeanMapper();
Person p2 = mapper.map(p1, Person.class);
p2.getAddress().setType(
"Office");
System.out.println(
"p1=" + p1);
System.out.println(
"p2=" + p2);
}
}
@Data
public
class Address {
private String type;
private String value;
}

输出:

p1=Person(       
name=Peter, age=
31, address=Address(
type=Home,
value=北京))
p2=Person(
name=Peter, age=
31, address=Address(
type=Office,
value=北京))

注意:在万次测试中dozer有一个很严重的问题,如果DozerBeanMapper对象在for循环中创建,效率(dozer:7358)降低近10倍。由于DozerBeanMapper是线程安全的,所以不应该每次都创建新的实例。可以自带的单例工厂DozerBeanMapperSingletonWrapper来创建mapper,或集成到spring中。

还有更暴力的,创建一个People类:

@Data      
public
class People {
private String name;
private String age;
//这里已经不是Integer了
private Address address;
@Test
public
void
testDozer
() {
Person p1=PersonFactory.newPrototypeInstance();
Mapper mapper =
new DozerBeanMapper();
People p2 = mapper.map(p1, People.class);
p2.getAddress().setType(
"Office");
System.out.println(
"p1=" + p1);
System.out.println(
"p2=" + p2);
}
}

只要属性名相同,干~

继续蹂躏:

@Data      
public
class People {
private String name;
private String age;
private Map
address;
//��
@Test
public
void
testDozer
() {
Person p1=PersonFactory.newPrototypeInstance();
Mapper mapper =
new DozerBeanMapper();
People p2 = mapper.map(p1, People.class);
p2.getAddress().put(
"type",
"Office");
System.out.println(
"p1=" + p1);
System.out.println(
"p2=" + p2);
}
}

利用Commons-BeanUtils复制对象

maven依赖

commons-beanutils
commons-beanutils
1.9.3

测试用例:

@Data      
public
class Person {
private String name;
private String age;
private Address address;
@Test
public
void
testCommonsBeanUtils
(){
Person p1=PersonFactory.newPrototypeInstance();
try {
Person p2=(Person) BeanUtils.cloneBean(p1);
System.out.println(
"p1=" + p1);
p2.getAddress().setType(
"Office");
System.out.println(
"p2=" + p2);
}
catch (Exception e) {
e.printStackTrace();
}
}
}

利用cglib复制对象

maven依赖:

cglib
cglib
3.2.4

测试用例:

@Test      
public
void
testCglib
(){
Person p1=PersonFactory.newPrototypeInstance();
BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class,
false);
Person p2=
new Person();
beanCopier.copy(p1, p2,
null);
p2.getAddress().setType(
"Office");
System.out.println(
"p1=" + p1);
System.out.println(
"p2=" + p2);
}

结果大跌眼镜,cglib这么牛x,居然是浅拷贝。不过cglib提供了扩展能力:

@Test      
public
void
testCglib
(){
Person p1=PersonFactory.newPrototypeInstance();
BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class,
true);
Person p2=
new Person();
beanCopier.copy(p1, p2,
new Converter(){
@Override
public
Object
convert
(Object value, Class target, Object context) {
if(target.isSynthetic()){
BeanCopier.create(target, target,
true).copy(value, value,
this);
}
return value;
}
});
p2.getAddress().setType(
"Office");
System.out.println(
"p1=" + p1);
System.out.println(
"p2=" + p2);
}

Orika复制对象

orika的作用不仅仅在于处理bean拷贝,更擅长各种类型之间的转换。

maven依赖:

ma.glasnost.orika
orika-core
1.5.0

测试用例:

@Test      
public
void
testOrika
() {
MapperFactory mapperFactory =
new DefaultMapperFactory.Builder().build();
mapperFactory.classMap(Person.class, Person.class)
.byDefault()
.register();
ConverterFactory converterFactory = mapperFactory.getConverterFactory();
MapperFacade mapper = mapperFactory.getMapperFacade();
Person p1=PersonFactory.newPrototypeInstance();
Person p2 = mapper.map(p1, Person.class);
System.out.println(
"p1=" + p1);
p2.getAddress().setType(
"Office");
System.out.println(
"p2=" + p2);
}

Spring BeanUtils复制对象

给Spring个面子,貌似它不支持深拷贝。

Person p1=PersonFactory.newPrototypeInstance();      
Person p2 =
new Person();
Person p2 = (Person) BeanUtils.cloneBean(p1);
//BeanUtils.copyProperties(p2, p1);//这个更没戏

深拷贝性能对比

@Test      
public
void
testBatchDozer
(){
Long start=System.currentTimeMillis();
Mapper mapper =
new DozerBeanMapper();
for(
int i=
0;i<
10000;i++){
Person p1=PersonFactory.newPrototypeInstance();
Person p2 = mapper.map(p1, Person.class);
}
System.out.println(
"dozer:"+(System.currentTimeMillis()-start));
//dozer:721
}
@Test
public
void
testBatchBeanUtils
(){
Long start=System.currentTimeMillis();
for(
int i=
0;i<
10000;i++){
Person p1=PersonFactory.newPrototypeInstance();
try {
Person p2=(Person) BeanUtils.cloneBean(p1);
}
catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(
"commons-beanutils:"+(System.currentTimeMillis()-start));
//commons-beanutils:229
}
@Test
public
void
testBatchCglib
(){
Long start=System.currentTimeMillis();
for(
int i=
0;i<
10000;i++){
Person p1=PersonFactory.newPrototypeInstance();
BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class,
true);
Person p2=
new Person();
beanCopier.copy(p1, p2,
new Converter(){
@Override
public
Object
convert
(Object value, Class target, Object context) {
if(target.isSynthetic()){
BeanCopier.create(target, target,
true).copy(value, value,
this);
}
return value;
}
});
}
System.out.println(
"cglib:"+(System.currentTimeMillis()-start));
//cglib:133
}
@Test
public
void
testBatchSerial
(){
Long start=System.currentTimeMillis();
for(
int i=
0;i<
10000;i++){
Person p1=PersonFactory.newPrototypeInstance();
Person p2=p1.deepClone();
}
System.out.println(
"serializable:"+(System.currentTimeMillis()-start));
//serializable:687
}
@Test
public
void
testBatchOrika
() {
MapperFactory mapperFactory =
new DefaultMapperFactory.Builder().build();
mapperFactory.classMap(Person.class, Person.class)
.field(
"name",
"name")
.byDefault()
.register();
ConverterFactory converterFactory = mapperFactory.getConverterFactory();
MapperFacade mapper = mapperFactory.getMapperFacade();
Long start=System.currentTimeMillis();
for(
int i=
0;i<
10000;i++){
Person p1=PersonFactory.newPrototypeInstance();
Person p2 = mapper.map(p1, Person.class);
}
System.out.println(
"orika:"+(System.currentTimeMillis()-start));
//orika:83
}
@Test
public
void
testBatchClone
(){
Long start=System.currentTimeMillis();
for(
int i=
0;i<
10000;i++){
Person p1=PersonFactory.newPrototypeInstance();
try {
Person p2=(Person) p1.clone();
}
catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
System.out.println(
"clone:"+(System.currentTimeMillis()-start));
//clone:8
}

(10k)性能比较:

//dozer:721      
//commons-beanutils:229
//cglib:133
//serializable:687
//orika:83
//clone:8

深拷贝总结

原生的clone效率无疑是最高的,用脚趾头都能想到。

偶尔用一次,用哪个都问题都不大。

一般性能要求稍高的应用场景,cglib和orika完全可以接受。

另外一个考虑的因素,如果项目已经引入了某个依赖,就用那个依赖来做吧,没必要再引入一个第三方依赖。

转载自

你可能感兴趣的文章
普通gpio口的申请和设置
查看>>
在kernel里添加一个i2c外围设备
查看>>
android lcd调试 高通平台lcd调试深入分析总结(mipi和rgb接口)
查看>>
高通平台开机logo连续显示调试总结
查看>>
Android display架构分析
查看>>
高通安卓调试LCD几方面总结(一)
查看>>
高通安卓调试LCD几方面总结(二)
查看>>
高通平台 lcd driver 调试小结
查看>>
开机logo切换逻辑深入研究
查看>>
高通平台手机开发之LCD
查看>>
高通平台修改LK(bootloader)开机logo
查看>>
lk启动流程详细分析
查看>>
ubuntu下无线网卡解决经历
查看>>
Android 开发工具安装步骤详解
查看>>
eclipse创建android项目出现error libz.so.1: cannot open shared object file:No such file or directory
查看>>
Ubuntu14.04下搜狗输入法安装
查看>>
高通平台bootloader显示logo图片的过程
查看>>
Ubuntu 下安装Source Insight
查看>>
Source Insight背景颜色设置成保护色
查看>>
Linux Framebuffer驱动剖析之一—软件需求
查看>>