每天进步一丢丢,连接梦与想
我们还年轻,但这不是你浪费青春的理由
克隆和复制
clone,有人称之为克隆,有人称之为复制,其实都是同一个东西
本文称之为"克隆",毕竟人家方法名叫"clone"
为什要用克隆
想一想,为什么需要克隆?为什么不重新new一个?道理很简单,目的是想要两个相同的对象,重新new一个还得自己重新赋值,太麻烦
如何克隆一个对象?
如果是个初学者,可能会这么写
1public class Student {
2 String name;
3
4 public Student(String name) {
5 super();
6 this.name = name;
7 }
8 public String getName() {
9 return name;
10 }
11 public void setName(String name) {
12 this.name = name;
13 }
14
15 public static void main(String[] args){
16 Student stu1 = new Student("小明");
17 Student stu2 = stu1;
18 }
19
20}
这确实是做了克隆,但只是克隆了引用变量
来验证一下
1 System.out.println("stu1:"+stu1.getName()+"
2 stu2:"+stu2.getName());
3 System.out.println("stu1 == stu2 : "+(stu1 == stu2));
4 //改名字
5 stu1.setName("小张");
6 System.out.println("改名后 stu1:"+stu1.getName()+"
7 stu2:"+stu2.getName());
8//输出
9stu1:小明 stu2:小明
10stu1 == stu2 : true
11改名后 stu1:小张 stu2:小张
修改了stu1的名字后,stu2的名字也随着改变
可以看出,两个引用stu1
和stu2
指向同一个对象
如图
你需要的是这样的克隆?
回想一下,平时真正需要的是两个不同对象
Object类中的clone
先来看下clone的源码,在Object类中
1/*
2Creates and returns a copy of this object. The precise meaning of "copy" may depend on the class of the object.
3The general intent is that, for any object x, the expression:
41) x.clone() != x will be true
52) x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.
63) x.clone().equals(x) will be true, this is not an absolute requirement.
7*/
8protected native Object clone() throws CloneNotSupportedException;
仔细看,它是个native方法,native方法是由非java语言实现的(因为java本身无法直接对操作底层进行访问和操作,需要通过其他语言实现)
注释主要说明了3点:
-
克隆对象和原对象不是同一个对象,占用不同的内存地址
-
克隆对象和原对象应该具有相同的类型,但它不是强制性的
-
克隆对象和原对象使用equals()方法比较应该是相等的,但它不是强制性的
因为每个类的基类都是Object,所以都有clone方法,但是它是protected,所以不能在类外访问
克隆一个对象,需要对clone重写
如何实现克隆
在说实现前,得区分下浅克隆和深克隆
-
浅克隆:原对象和克隆对象不同,但对象内的成员引用相同
-
深克隆:原对象和克隆对象不同,且对象内的成员引用也不同
不同:不是同一个对象,所占内存地址不同
成员引用:类中为引用类型的成员
以图说明,更形象些男孩比喻为一个类,电脑比喻为类中的成员引用
-
一个男孩拥有一台电脑,通过浅克隆后,成了两个男孩,但他们共享一台电脑
-
一个男孩拥有一台电脑,通过深克隆后,成了两个男孩,他们拥有各自的电脑
浅克隆
1//学生类
2public class Student implements Cloneable{
3 private String name;
4 private Integer age;
5 private Bag bag;
6
7 public Student(String name,Integer age,Bag bag) {
8 this.name = name;
9 this.age = age;
10 this.bag = bag;
11 }
12
13 public String getName() {
14 return name;
15 }
16
17 public void setName(String name) {
18 this.name = name;
19 }
20
21 public Integer getAge() {
22 return age;
23 }
24
25 public void setAge(Integer age) {
26 this.age = age;
27 }
28
29 public Bag getBag() {
30 return bag;
31 }
32
33 public void setBag(Bag bag) {
34 this.bag = bag;
35 }
36
37 @Override
38 public Student clone(){
39 Student stu = null;
40 try{
41 stu = (Student)super.clone();
42 } catch (CloneNotSupportedException e){
43 e.printStackTrace();
44 }
45 return stu;
46 }
47
48 @Override
49 public String toString() {
50 return "Student{" +
51 "name='" + name + '\'' +
52 ", age=" + age +
53 ", bag=" + bag.getName() +
54 '}';
55 }
56}
1//背包类
2public class Bag {
3 private String name;
4
5 public Bag(String name) {
6 this.name = name;
7 }
8
9 public String getName() {
10 return name;
11 }
12
13 public void setName(String name) {
14 this.name = name;
15 }
16}
1//测试类
2public class Test {
3 public static void main(String[] args){
4 Student stu1 = new Student("小明",25,new Bag("小明的背包"));
5 Student stu2 = stu1.clone();
6 System.out.println("两对象是否相等");
7 System.out.println("stu1 == stu2 "+(stu1 == stu2));
8 System.out.println("stu1 "+stu1.toString());
9 System.out.println("stu2 "+stu2.toString());
10 System.out.println("对象内引用成员是否相等");
11 System.out.println("stu1.name == stu2.name "+ (stu1.getName() == stu2.getName()));
12 System.out.println("stu1.age == stu2.age "+(stu1.getAge() == stu2.getAge()));
13 System.out.println("stu1.bag == stu2.bag "+(stu1.getBag() == stu2.getBag()));
14 }
15}
16
17//输出
18两对象是否是同一对象
19stu1 == stu2 false
20stu1 Student{name='小明', age=25, bag=小明的背包}
21stu2 Student{name='小明', age=25, bag=小明的背包}
22对象内引用成员是否相等
23stu1.name == stu2.name true
24stu1.age == stu2.age true
25stu1.bag == stu2.bag true
可看出,原对象和克隆对象不是同一对象
,克隆对象内的值与原对象相同;对象内引用成员相等,说明只做了引用克隆,不同引用指向同一对象
1//改变stu1类中成员的值
2stu1.setName("小张");
3stu1.setAge(18);
4stu1.getBag().setName("小张的背包");
5System.out.println("stu1 "+stu1.toString());
6System.out.println("stu2 "+stu2.toString());
7
8//输出
9stu1 Student{name='小张', age=18, bag=小张的背包}
10stu2 Student{name='小明', age=25, bag=小张的背包}
stu1改变bag的名称,stu2中bag会同时改变,因为两个bag指向的是同一个对象
但name,age成员为何没有跟着改变?因为它们的类型分别是String和Integer,String,Integer是不可变类,不可改变原值,对它赋值就等同于让它指向另一个新对象,其余的七种基本数据类型的包装类也一样
。
深克隆
有两种实现方法
-
多层实现Cloneable类
-
利用序列化和反序列化
1.多层实现Cloneable类
让上述的Bag类也实现Cloneable类,并重写clone方法
1public class Bag implements Cloneable{
2 private String name;
3
4 public Bag(String name) {
5 this.name = name;
6 }
7
8 public String getName() {
9 return name;
10 }
11
12 public void setName(String name) {
13 this.name = name;
14 }
15
16 @Override
17 public Bag clone(){
18 Bag bag= null;
19 try{
20 bag= (Bag )super.clone();
21 } catch (CloneNotSupportedException e){
22 e.printStackTrace();
23 }
24 return bag;
25 }
26}
且在Student类的clone方法中执行Bag的clone方法
1 @Override
2 public Student clone(){
3 Student stu = null;
4 try{
5 //浅克隆
6 stu = (Student)super.clone();
7 } catch (CloneNotSupportedException e){
8 e.printStackTrace();
9 }
10 //深克隆
11 stu.bag = (Bag)bag.clone();
12 return stu;
13 }
这样便可实现深克隆,但这种方法很麻烦,若Bag类中还含有成员引用,则又需要再让它实现Cloneable接口重写clone方法,这样代码会显得很臃肿,且繁琐。
还是第二种方法简单易用,来瞧一瞧
2.利用序列化和反序列化实现深克隆
1public class Student implements Serializable {
2 private String name;
3 private Integer age;
4 private Bag bag;
5
6 ...
7
8 public Student myClone(){
9 Student stu = null;
10 try {
11 //将对象序列化到流里
12 ByteArrayOutputStream os = new ByteArrayOutputStream();
13 ObjectOutputStream oos = new ObjectOutputStream(os);
14 oos.writeObject(this);
15 //将流反序列化成对象
16 ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
17 ObjectInputStream ois = new ObjectInputStream(is);
18 stu = (Student) ois.readObject();
19 } catch (IOException e) {
20 e.printStackTrace();
21 } catch (ClassNotFoundException e) {
22 e.printStackTrace();
23 }
24 return stu;
25 }
26}
需要注意的是成员引用也需要实现Serializable接口
1public class Bag implements Serializable {...
这种方法是利用序列化对象后可将其拷贝到流里,而原对象仍在jvm中,然后从流中将其反序列化成另一个对象到jvm中,从而实现深克隆
总结
-
克隆可分为浅克隆和深克隆,实际应用中一般使用深克隆
-
深克隆有两种实现方法
-
实现Cloneable接口
-
利用序列化和反序列化(简单方便)
扩展
Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。