Java提高篇——对象克隆(复制)

阅读目录

  • 为什么要克隆?
  • 如何实现克隆
  • 浅克隆和深克隆
  • 解决多层克隆问题
  • 总结

假如说你想复制一个简单变量。很简单:

int apples = 5;
int pears = apples;  

不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。

但是如果你复制的是一个对象,情况就有些复杂了。

假设说我是一个beginner,我会这样写:

class Student {  private int number;
</span><span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">public</span> <span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">int</span><span style="margin:0px;padding:0px;line-height:1.8;"> getNumber() {  </span><span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">return</span><span style="margin:0px;padding:0px;line-height:1.8;"> number;
}  </span><span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">public</span> <span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">void</span> setNumber(<span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">int</span><span style="margin:0px;padding:0px;line-height:1.8;"> number) {  </span><span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">this</span>.number =<span style="margin:0px;padding:0px;line-height:1.8;"> number;
}

}
public class Test {

</span><span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">public</span> <span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">static</span> <span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">void</span><span style="margin:0px;padding:0px;line-height:1.8;"> main(String args[]) {  Student stu1 </span>= <span style="margin:0px;padding:0px;line-height:1.8;color:rgb(0,0,255);">new</span><span style="margin:0px;padding:0px;line-height:1.8;"> Student();  stu1.setNumber(</span>12345<span style="margin:0px;padding:0px;line-height:1.8;">);  Student stu2 </span>=<span style="margin:0px;padding:0px;line-height:1.8;"> stu1;  System.out.println(</span>"学生1:" +<span style="margin:0px;padding:0px;line-height:1.8;"> stu1.getNumber());  System.out.println(</span>"学生2:" +<span style="margin:0px;padding:0px;line-height:1.8;"> stu2.getNumber());
}

}

结果:

学生1:12345

学生2:12345

这里我们自定义了一个学生类,该类只有一个number字段。

我们新建了一个学生实例,然后将该值赋值给stu2实例。(Student stu2 = stu1;)

再看看打印结果,作为一个新手,拍了拍胸腹,对象复制不过如此,

难道真的是这样吗?

我们试着改变stu2实例的number字段,再打印结果看看:

stu2.setNumber(54321);

System.out.println(“学生1:” + stu1.getNumber());
System.out.println(“学生2:” + stu2.getNumber());

结果:

学生1:54321

学生2:54321

这就怪了,为什么改变学生2的学号,学生1的学号也发生了变化呢?

原因出在(stu2 = stu1) 这一句。该语句的作用是将stu1的引用赋值给stu2,

这样,stu1和stu2指向内存堆中同一个对象。如图:

那么,怎样才能达到复制一个对象呢?

是否记得万类之王Object。它有11个方法,有两个protected的方法,其中一个为clone方法。

在Java中所有的类都是缺省的继承自Java语言包中的Object类的,查看它的源码,你可以把你的JDK目录下的src.zip复制到其他地方然后解压,里面就是所有的源码。发现里面有一个访问限定符为protected的方法clone():

/*
Creates and returns a copy of this object. The precise meaning of “copy” may depend on the class of the object.
The general intent is that, for any object x, the expression:
  1. x.clone() != x will be true

  2. x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.

  3. x.clone().equals(x) will be true, this is not an absolute requirement.
    */
    protected native Object clone() throws CloneNotSupportedException;

    仔细一看,它还是一个native方法,大家都知道native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。

    1. 第一次声明保证克隆对象将有单独的内存地址分配。
    2. 第二次声明表明,原始和克隆的对象应该具有相同的类类型,但它不是强制性的。
    3. 第三声明表明,原始和克隆的对象应该是平等的equals()方法使用,但它不是强制性的。

    因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。

    要想对一个对象进行复制,就需要对clone方法覆盖。

    回到顶部

    为什么要克隆?

      大家先思考一个问题,为什么需要克隆对象?直接new一个对象不行吗?

      答案是:克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。那么我把这个对象的临时属性一个一个的赋值给我新new的对象不也行嘛?可以是可以,但是一来麻烦不说,二来,大家通过上面的源码都发现了clone是一个native方法,就是快啊,在底层实现的。

      提个醒,我们常见的Object a=new Object();Object b;b=a;这种形式的代码复制的是引用,即对象在内存中的地址,a和b对象仍然指向了同一个对象。

      而通过clone方法赋值的对象跟原来的对象时同时独立存在的。

    回到顶部

    如何实现克隆

    先介绍一下两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。

    在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。

    一般步骤是(浅克隆):

    1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口(不含任何方法)

    2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。(native为本地方法)

    下面对上面那个方法进行改造:

    class Student implements Cloneable{
    private int number;
    public int getNumber() {
    return number;
    }
    public void setNumber(int number) {
    this.number = number;
    }
    @Override
    public Object clone() {
    Student stu = null;
    try{
    stu = (Student)super.clone();
    }catch(CloneNotSupportedException e) {
    e.printStackTrace();
    }
    return stu;
    }
    }
    public class Test {
    public static void main(String args[]) {
    Student stu1 = new Student();
    stu1.setNumber(12345);
    Student stu2 = (Student)stu1.clone();
    
     System.out.println(</span>"学生1:" +<span style="margin:0px;padding:0px;line-height:1.8;"> stu1.getNumber());  System.out.println(</span>"学生2:" +<span style="margin:0px;padding:0px;line-height:1.8;"> stu2.getNumber());  stu2.setNumber(</span>54321<span style="margin:0px;padding:0px;line-height:1.8;">);  System.out.println(</span>"学生1:" +<span style="margin:0px;padding:0px;line-height:1.8;"> stu1.getNumber());  System.out.println(</span>"学生2:" +<span style="margin:0px;padding:0px;line-height:1.8;"> stu2.getNumber());
    
    }
    }  

    结果:

    学生1:12345

    学生2:12345

    学生1:12345

    学生2:54321

    如果你还不相信这两个对象不是同一个对象,那么你可以看看这一句:

    System.out.println(stu1 == stu2); // false  

    上面的复制被称为浅克隆。

    还有一种稍微复杂的深度复制:

    我们在学生类里再加一个Address类。

     1 class Address  {2     private String add;34     public String getAdd() {5         return add;6     }78     public void setAdd(String add) {9         this.add = add;
    10     }
    11
    12 }
    13
    14 class Student implements Cloneable{
    15     private int number;
    16
    17     private Address addr;
    18
    19     public Address getAddr() {
    20         return addr;
    21     }
    22
    23     public void setAddr(Address addr) {
    24         this.addr = addr;
    25     }
    26
    27     public int getNumber() {
    28         return number;
    29     }
    30
    31     public void setNumber(int number) {
    32         this.number = number;
    33     }
    34
    35     @Override
    36     public Object clone() {
    37         Student stu = null;
    38         try{
    39             stu = (Student)super.clone();
    40         }catch(CloneNotSupportedException e) {
    41             e.printStackTrace();
    42         }
    43         return stu;
    44     }
    45 }
    46 public class Test {
    47
    48     public static void main(String args[]) {
    49
    50         Address addr = new Address();
    51         addr.setAdd(“杭州市”);
    52         Student stu1 = new Student();
    53         stu1.setNumber(123);
    54         stu1.setAddr(addr);
    55
    56         Student stu2 = (Student)stu1.clone();
    57
    58         System.out.println(“学生1:” + stu1.getNumber() + “,地址:” + stu1.getAddr().getAdd());
    59         System.out.println(“学生2:” + stu2.getNumber() + “,地址:” + stu2.getAddr().getAdd());
    60     }
    61 }  

    结果:

    学生1:123,地址:杭州市

    学生2:123,地址:杭州市

    乍一看没什么问题,真的是这样吗?

    我们在main方法中试着改变addr实例的地址。

    addr.setAdd(“西湖区”);
    

System.out.println(“学生1:” + stu1.getNumber() + “,地址:” + stu1.getAddr().getAdd());
System.out.println(“学生2:” + stu2.getNumber() + “,地址:” + stu2.getAddr().getAdd());

结果:

学生1:123,地址:杭州市
学生2:123,地址:杭州市
学生1:123,地址:西湖区
学生2:123,地址:西湖区  

这就奇怪了,怎么两个学生的地址都改变了?

原因是浅复制只是复制了addr变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。

所以,为了达到真正的复制对象,而不是纯粹引用复制。我们需要将Address类可复制化,并且修改clone方法,完整代码如下:

 1 package abc;23 class Address implements Cloneable {4     private String add;56     public String getAdd() {7         return add;8     }9
10     public void setAdd(String add) {
11         this.add = add;
12     }
13
14     @Override
15     public Object clone() {
16         Address addr = null;
17         try{
18             addr = (Address)super.clone();
19         }catch(CloneNotSupportedException e) {
20             e.printStackTrace();
21         }
22         return addr;
23     }
24 }
25
26 class Student implements Cloneable{
27     private int number;
28
29     private Address addr;
30
31     public Address getAddr() {
32         return addr;
33     }
34
35     public void setAddr(Address addr) {
36         this.addr = addr;
37     }
38
39     public int getNumber() {
40         return number;
41     }
42
43     public void setNumber(int number) {
44         this.number = number;
45     }
46
47     @Override
48     public Object clone() {
49         Student stu = null;
50         try{
51             stu = (Student)super.clone();   //浅复制
52         }catch(CloneNotSupportedException e) {
53             e.printStackTrace();
54         }
55         stu.addr = (Address)addr.clone();   //深度复制
56         return stu;
57     }
58 }
59 public class Test {
60
61     public static void main(String args[]) {
62
63         Address addr = new Address();
64         addr.setAdd(“杭州市”);
65         Student stu1 = new Student();
66         stu1.setNumber(123);
67         stu1.setAddr(addr);
68
69         Student stu2 = (Student)stu1.clone();
70
71         System.out.println(“学生1:” + stu1.getNumber() + “,地址:” + stu1.getAddr().getAdd());
72         System.out.println(“学生2:” + stu2.getNumber() + “,地址:” + stu2.getAddr().getAdd());
73
74         addr.setAdd(“西湖区”);
75
76         System.out.println(“学生1:” + stu1.getNumber() + “,地址:” + stu1.getAddr().getAdd());
77         System.out.println(“学生2:” + stu2.getNumber() + “,地址:” + stu2.getAddr().getAdd());
78     }
79 }  

结果:

学生1:123,地址:杭州市
学生2:123,地址:杭州市
学生1:123,地址:西湖区
学生2:123,地址:杭州市  

这样结果就符合我们的想法了。

最后我们可以看看API里其中一个实现了clone方法的类:

java.util.Date:

/**
  • Return a copy of this object.
    */
    public Object clone() {
    Date d = null;
    try {
    d = (Date)super.clone();
    if (cdate != null) {
    d.cdate = (BaseCalendar.Date) cdate.clone();
    }
    } catch (CloneNotSupportedException e) {} // Won’t happen
    return d;
    }

    该类其实也属于深度复制。

    参考文档:Java如何复制对象

    回到顶部

    浅克隆和深克隆

    1、浅克隆

    在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

    简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

    在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。

    2、深克隆

    在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。

    简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

    在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化(Serialization)等方式来实现。

    (如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。)

    序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

    扩展
    Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。
    回到顶部

    解决多层克隆问题

    如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。这时我们可以用序列化的方式来实现对象的深克隆。

     1 public class Outer implements Serializable{2   private static final long serialVersionUID = 369285298572941L;  //最好是显式声明ID3   public Inner inner;4  //Discription:[深度复制方法,需要对象及对象所有的对象属性都实现序列化] 5   public Outer myclone() {6       Outer outer = null;7       try { // 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
     8           ByteArrayOutputStream baos = new ByteArrayOutputStream();9           ObjectOutputStream oos = new ObjectOutputStream(baos);
    10           oos.writeObject(this);
    11       // 将流序列化成对象
    12           ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    13           ObjectInputStream ois = new ObjectInputStream(bais);
    14           outer = (Outer) ois.readObject();
    15       } catch (IOException e) {
    16           e.printStackTrace();
    17       } catch (ClassNotFoundException e) {
    18           e.printStackTrace();
    19       }
    20       return outer;
    21   }
    22 }

    Inner也必须实现Serializable,否则无法序列化:

     1 public class Inner implements Serializable{2   private static final long serialVersionUID = 872390113109L; //最好是显式声明ID3   public String name = “”;45   public Inner(String name) {6       this.name = name;7   }89   @Override
    10   public String toString() {
    11       return “Inner的name值为:” + name;
    12   }
    13 }

    这样也能使两个对象在内存空间内完全独立存在,互不影响对方的值。

    回到顶部

    总结

    实现对象克隆有两种方式:

      1). 实现Cloneable接口并重写Object类中的clone()方法;

      2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

    注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来总是优于把问题留到运行时。

    文章来源:https://blog.csdn.net/m0_37601917/article/details/80741773

Java提高篇——对象克隆相关推荐

  1. 【转】java提高篇(十)-----详解匿名内部类

    原文网址:http://www.cnblogs.com/chenssy/p/3390871.html 在java提高篇-----详解内部类中对匿名内部类做了一个简单的介绍,但是内部类还存在很多其他细节 ...

  2. java提高篇(三十)-----Iterator

    本文转载地址:            http://blog.csdn.net/chenssy/article/details/37521461 迭代对于我们搞Java的来说绝对不陌生.我们常常使用J ...

  3. java提高篇(八)----详解内部类

    可以将一个类的定义放在另一个类的定义内部,这就是内部类. 内部类是一个非常有用的特性但又比较难理解使用的特性(鄙人到现在都没有怎么使用过内部类,对内部类也只是略知一二). 第一次见面 内部类我们从外面 ...

  4. java提高篇之数组(2)

    前面一节主要介绍了数组的基本概念,对什么是数组稍微深入了一点点,在这篇博文中主要介绍数组的其他方面. 三.性能?请优先考虑数组 在java中有很多方式来存储一系列数据,而且在操作上面比数组方便的多?但 ...

  5. java提高篇之详解内部类

    转载自 java提高篇之详解内部类 内部类是一个非常有用的特性但又比较难理解使用的特性(鄙人到现在都没有怎么使用过内部类,对内部类也只是略知一二). 第一次见面 内部类我们从外面看是非常容易理解的,无 ...

  6. java提高篇之抽象类与接口

    转载自 java提高篇之抽象类与接口 接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法. 抽象类与接口是java语言中对抽象概念进行定义的两种机制,正是由于他们的存在才赋予java强大的 ...

  7. 【转】java提高篇(二)-----理解java的三大特性之继承

    [转]java提高篇(二)-----理解java的三大特性之继承 原文地址:http://www.cnblogs.com/chenssy/p/3354884.html 在<Think in ja ...

  8. Java提高篇 —— Java三大特性之继承

    一.前言 在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事 ...

  9. java迭代器cas,java提高篇(三十)-Iterator - Java 技术驿站-Java 技术驿站

    迭代对于我们搞Java的来说绝对不陌生.我们常常使用JDK提供的迭代接口进行Java集合的迭代. Iterator iterator = list.iterator(); while(iterator ...

最新文章

  1. 我看UNIX与Windows的本质区别
  2. spring tx:advice 和 aop:config 配置事务
  3. 牛客练习赛46T1-华华教奕奕写几何【数学】
  4. 算法工程师和python_算法工程师只掌握Python行吗?如果在java和cpp中选一门语言学习哪个更有用?...
  5. java 根据类名示例化类_Java即时类| EpochSecond()方法的示例
  6. 前端学习(2713):重读vue电商网站33之实现首页路由重定向
  7. JVM 的 Finalization Delay 引起的 OOM(java.lang.OutOfMemoryError:null at sun.misc.Unsafe.allocateMemory.)
  8. php找不到phpmyadmin,phpMyAdmin 安装配置方法和问题解决
  9. android 音乐app 进度条_让这些可爱的APP成为你的生活好帮手
  10. java 方法 时间_Java 方法
  11. java Vector.toArray 与强制类型转换
  12. 无法访问移动磁盘显示磁盘未被格式化的文件寻回方案
  13. matlab 有约束最小化,求解带等式约束和最小化目标的LMI
  14. cadz轴归零命令_cadz轴归零(cad全部z轴归零)
  15. Android 梯形进度条、下载进度条;
  16. 南科大副教授“跳槽”到深圳中学引热议!大学老师不香了吗?
  17. 从零开始学习Java设计模式 | 创建型模式篇:建造者模式
  18. 信息学奥赛第九节 —— 贪心算法(需要安排几位师傅加工零件 + 排队打水问题)
  19. chromedriver与chrome浏览器各版本对应下载
  20. 支付功能----第三方支付公司

热门文章

  1. matlab彩色图添加水印,基于离散小波变换的彩色图像水印算法及其MATLAB实现
  2. 现在这社会都集体高潮了
  3. 计算机化系统验证管理制度,计算机化系统验证(csv)文件清单
  4. 一个java程序员的真实经历
  5. jenkins安装 插件插件失败 简单快捷安装办法
  6. Vivado无法识别开发板解决办法
  7. ffmpeg每隔30帧抽取1帧画面
  8. oracle database的asm磁盘管理
  9. Web应用程序漏洞-POST任意文件下载
  10. go 打包到不同平台windows linux运行程序 压缩程序体积