java對象拷貝詳解及實例
Java賦值是復制對象引用,如果我們想要得到一個對象的副本,使用賦值操作是無法達到目的的:
1
2
3
4
5
6
7
8
9
|
@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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@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類的成員:
1
2
3
4
5
|
@Data public class Address { private String type; private String value; } |
再來測試,問題來了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@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); } |
查看輸出:
1
2
3
|
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()方法。
1
2
3
4
5
6
7
8
9
10
|
@Data public class Address implements Cloneable { private String type; private String value; @Override protected Object clone() throws CloneNotSupportedException { return super .clone(); } } |
這樣還不夠,Person的clone()需要顯式地clone其引用成員。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@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; } } |
重新跑前面的測試用例:
1
2
3
|
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()方法。因此編碼過程中比較容易忽略其中一個環節,對于復雜的項目排查就是困難了。
要尋找可靠的,簡單的方法,序列化就是一種途徑。
1.被復制對象的繼承鏈、引用鏈上的每一個對象都實現java.io.Serializable接口。這個比較簡單,不需要實現任何方法,serialVersionID的要求不強制,對深拷貝來說沒毛病。
2.實現自己的deepClone方法,將this寫入流,再讀出來。俗稱:冷凍-解凍。
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
|
@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; } } |
原型工廠類
為了便于測試,也節省篇幅,封裝一個工廠類。
公平起見,避免某些工具庫使用緩存機制,使用原型方式工廠。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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依賴
1
2
3
4
5
|
< dependency > < groupId >net.sf.dozer</ groupId > < artifactId >dozer</ artifactId > < version >5.5.1</ version > </ dependency > |
測試用例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@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; } |
輸出:
1
2
|
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類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@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); } } |
只要屬性名相同,干~
繼續蹂躪:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Data public class People { private String name; private String age; private Map<String,String> 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依賴
1
2
3
4
5
|
< dependency > < groupId >commons-beanutils</ groupId > < artifactId >commons-beanutils</ artifactId > < version >1.9.3</ version > </ dependency > |
測試用例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@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依賴:
1
2
3
4
5
|
< dependency > < groupId >cglib</ groupId > < artifactId >cglib</ artifactId > < version >3.2.4</ version > </ dependency > |
測試用例:
1
2
3
4
5
6
7
8
9
10
|
@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提供了擴展能力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@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依賴:
1
2
3
4
5
6
|
<dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version> 1.5 . 0 </version> </dependency> </dependencies> |
測試用例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@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個面子,貌似它不支持深拷貝。
1
2
3
4
|
Person p1=PersonFactory.newPrototypeInstance(); Person p2 = new Person(); Person p2 = (Person) BeanUtils.cloneBean(p1); //BeanUtils.copyProperties(p2, p1);//這個更沒戲 |
深拷貝性能對比
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
@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)性能比較:
1
2
3
4
5
6
|
//dozer:721 //commons-beanutils:229 //cglib:133 //serializable:687 //orika:83 //clone:8 |
深拷貝總結
原生的clone效率無疑是最高的,用腳趾頭都能想到。
偶爾用一次,用哪個都問題都不大。
一般性能要求稍高的應用場景,cglib和orika完全可以接受。
另外一個考慮的因素,如果項目已經引入了某個依賴,就用那個依賴來做吧,沒必要再引入一個第三方依賴。
感謝閱讀,希望能幫助到大家,謝謝大家對本站的支持!
原文鏈接:http://blog.csdn.net/54powerman/article/details/64920431