3.3各种拷贝数据比较
目录介绍
- 01.对象拷贝有哪些
- 1.1 为何需要拷贝
- 1.2 数据拷贝的场景
- 1.3 拷贝类型有哪些
- 02.为何要用浅拷贝
- 2.1 什么是浅拷贝
- 2.2 实现浅拷贝案例
- 2.3 浅拷贝使用场景
- 03.为何要用深拷贝
- 3.1 什么是深拷贝
- 3.2 实现深拷贝案例
- 3.3 序列化属于深拷贝
- 3.4 序列化注意要点
- 3.5 序列化拷贝案例
- 04.什么是延迟拷贝
- 4.1 什么是延迟拷贝
- 4.2 延迟拷贝原理
- 4.3 延迟拷贝使用场景
- 06.数据拷贝案例
- 6.1 基本数据类型数组
- 6.2 引用数据类型数组
- 6.3 集合浅拷贝
- 6.4 集合深拷贝
- 07.如何选择拷贝方式
01.对象拷贝有哪些
1.1 为何需要拷贝
- 在Java中,拷贝(Copy)操作是常见的,它涉及将一个对象的值复制到另一个对象中。拷贝操作在许多情况下是有用的:
- 防止数据修改:通过拷贝对象,可以创建一个新的对象,使其具有相同的值。如果对其中一个对象进行修改,不会影响到原始对象。这在需要保护数据完整性的情况下很有用,特别是当多个对象需要独立操作相同数据时。
- 传递不可变性:在Java中,字符串(String)和基本数据类型(如整数、浮点数等)是不可变的。当需要将这些不可变对象传递给其他方法或对象时,拷贝操作可以确保传递的是对象的副本,而不是引用。这样可以防止外部修改原始对象。
- 多线程安全:在多线程环境下,如果多个线程需要同时访问同一个对象,为了避免竞态条件和数据不一致的问题,可以使用拷贝操作创建每个线程的私有副本。这样每个线程都可以独立地操作自己的副本,而不会影响其他线程。
- 数据备份:有时候需要对数据进行备份,以便在需要时可以还原到之前的状态。通过拷贝操作,可以创建数据的副本,以备份或存档目的。
- Java中的拷贝操作是为了保护数据完整性、传递不可变性、实现多线程安全以及进行数据备份等目的。
- 通过拷贝操作,可以创建对象的副本,使其具有独立的状态,以满足不同的需求。
1.2 数据拷贝的场景
- 在多线程环境下,多个线程可能同时访问和修改共享的数据。
- 为了避免竞态条件和数据不一致的问题,可以使用数据拷贝创建每个线程的私有副本。这样每个线程都可以独立地操作自己的数据副本,而不会影响其他线程。
- 数据传递,当需要将数据传递给其他方法、对象或线程时会使用拷贝
- 通过数据拷贝可以确保传递的是数据的副本,而不是引用。这样可以防止外部修改原始数据,保持数据的不可变性和安全性。
- 数据备份和还原:有时候需要对数据进行备份,以便在需要时可以还原到之前的状态。
- 通过数据拷贝,可以创建数据的副本,以备份或存档目的。这对于数据的恢复、回滚或历史记录等操作非常有用。
- 数据缓存:在某些情况下,为了提高性能,可以使用数据拷贝将数据缓存到内存中
- 这样可以避免频繁地从磁盘或网络中读取数据,提高数据访问的速度。
1.3 拷贝类型有哪些
- 对象拷贝(Object Copy)就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去。在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用对象的部分或全部数据。
- Java中有三种类型的对象拷贝:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)、延迟拷贝(Lazy Copy)。
02.理解浅拷贝
2.1 什么是浅拷贝
- 浅拷贝(Shallow Copy):浅拷贝创建一个新对象,该对象与原始对象共享相同的引用类型属性。
- 换句话说,浅拷贝只复制对象的引用,而不复制引用指向的实际对象。
- 这意味着对于引用类型属性的修改会影响到原始对象和副本对象。在Java中,可以使用clone()方法来实现浅拷贝。
2.2 如何实现浅拷贝
- 下面来看一看实现浅拷贝的一个例子
public class Subject { private String name; public Subject(String s) { name = s; } public String getName() { return name; } public void setName(String s) { name = s; } }
- 如下所示,定义Student类
public class Student implements Cloneable { // 对象引用 private Subject subj; private String name; public Student(String s, String sub) { name = s; subj = new Subject(sub); } public Subject getSubj() { return subj; } public String getName() { return name; } public void setName(String s) { name = s; } /** * 重写clone()方法 * @return */ public Object clone() { //浅拷贝 try { // 直接调用父类的clone()方法 return super.clone(); } catch (CloneNotSupportedException e) { return null; } } }
- 如下所示,测试
private void test1(){ // 原始对象 Student stud = new Student("杨充", "潇湘剑雨"); System.out.println("原始对象: " + stud.getName() + " - " + stud.getSubj().getName()); // 拷贝对象 Student clonedStud = (Student) stud.clone(); System.out.println("拷贝对象: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName()); // 原始对象和拷贝对象是否一样: System.out.println("原始对象和拷贝对象是否一样: " + (stud == clonedStud)); // 原始对象和拷贝对象的name属性是否一样 System.out.println("原始对象和拷贝对象的name属性是否一样: " + (stud.getName() == clonedStud.getName())); // 原始对象和拷贝对象的subj属性是否一样 System.out.println("原始对象和拷贝对象的subj属性是否一样: " + (stud.getSubj() == clonedStud.getSubj())); stud.setName("小杨逗比"); stud.getSubj().setName("潇湘剑雨大侠"); System.out.println("更新后的原始对象: " + stud.getName() + " - " + stud.getSubj().getName()); System.out.println("更新原始对象后的克隆对象: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName()); }
- 输出结果如下:
2019-03-23 13:50:57.518 24704-24704/com.ycbjie.other I/System.out: 原始对象: 杨充 - 潇湘剑雨 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 拷贝对象: 杨充 - 潇湘剑雨 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始对象和拷贝对象是否一样: false 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始对象和拷贝对象的name属性是否一样: true 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始对象和拷贝对象的subj属性是否一样: true 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 更新后的原始对象: 小杨逗比 - 潇湘剑雨大侠 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 更新原始对象后的克隆对象: 杨充 - 潇湘剑雨大侠
- 可以得出的结论
- 在这个例子中,让要拷贝的类Student实现了Cloneable接口并重写Object类的clone()方法,然后在方法内部调用super.clone()方法。
- 从输出结果中我们可以看到,对原始对象stud的"name"属性所做的改变并没有影响到拷贝对象clonedStud,但是对引用对象subj的"name"属性所做的改变影响到了拷贝对象clonedStud。
2.3 浅拷贝使用场景
- 浅拷贝(Shallow Copy)在Java中有一些使用场景,其中一些常见的场景包括:
- 缓存复制:当需要在多个地方使用相同的对象时,可以使用浅拷贝来创建对象的副本,以避免重复创建相同的对象。这样可以提高性能并减少内存消耗。
- 原型模式:浅拷贝在原型模式中非常有用。原型模式是一种创建对象的设计模式,其中通过复制现有对象来创建新对象,而不是使用昂贵的实例化过程。浅拷贝可以用于创建原型对象的副本,然后根据需要进行修改。
- 需要注意的是,浅拷贝只复制对象本身和对象中的基本类型字段,而对于引用类型字段,只是复制了引用,而不是创建新的对象。
- 因此,在使用浅拷贝时,需要注意共享对象的修改可能会影响到原始对象和浅拷贝对象。
03.为何要用深拷贝
3.1 什么是深拷贝
- 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
image - 在上图中,SourceObject有一个int类型的属性 "field1"和一个引用类型属性"refObj1"(引用ContainedObject类型的对象)。
- 当对SourceObject做深拷贝时,创建了CopiedObject,它有一个包含"field1"拷贝值的属性"field2"以及包含"refObj1"拷贝值的引用类型属性"refObj2"。
- 因此对SourceObject中的"refObj"所做的任何改变都不会影响到CopiedObject
3.2 实现深拷贝案例
- 下面是实现深拷贝的一个例子。只是在浅拷贝的例子上做了一点小改动,Subject 和CopyTest 类都没有变化。
public class Student implements Cloneable { // 对象引用 private Subject subj; private String name; public Student(String s, String sub) { name = s; subj = new Subject(sub); } public Subject getSubj() { return subj; } public String getName() { return name; } public void setName(String s) { name = s; } /** * 重写clone()方法 * * @return */ public Object clone() { // 深拷贝,创建拷贝类的一个新对象,这样就和原始对象相互独立 Student s = new Student(name, subj.getName()); return s; } }
- 输出结果如下:
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象: 杨充 - 潇湘剑雨 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 拷贝对象: 杨充 - 潇湘剑雨 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象和拷贝对象是否一样: false 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象和拷贝对象的name属性是否一样: true 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象和拷贝对象的subj属性是否一样: false 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 更新后的原始对象: 小杨逗比 - 潇湘剑雨大侠 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 更新原始对象后的克隆对象: 杨充 - 潇湘剑雨
- 得出的结论
- 很容易发现clone()方法中的一点变化。因为它是深拷贝,所以你需要创建拷贝类的一个对象。
- 因为在Student类中有对象引用,所以需要在Student类中实现Cloneable接口并且重写clone方法。
3.3 序列化属于深拷贝
- 可能你会问,序列化是属于那种类型拷贝?
- 答案是:通过序列化来实现深拷贝。可以思考一下,为何序列化对象要用深拷贝而不是用浅拷贝呢?
3.4 序列化注意要点
- 序列化是干什么的?
- 它将整个对象图写入到一个持久化存储文件中并且当需要的时候把它读取回来, 这意味着当你需要把它读取回来时你需要整个对象图的一个拷贝。
- 这就是当你深拷贝一个对象时真正需要的东西。请注意,当你通过序列化进行深拷贝时,必须确保对象图中所有类都是可序列化的。
3.5 序列化拷贝案例
- 看一下下面案例,很简单,只需要实现Serializable这个接口。Android中还可以实现Parcelable接口。
public class ColoredCircle implements Serializable { private int x; private int y; public ColoredCircle(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } @Override public String toString() { return "x=" + x + ", y=" + y; } }
- 测试
private void test3() { ObjectOutputStream oos = null; ObjectInputStream ois = null; try { // 创建原始的可序列化对象 DouBi c1 = new DouBi(100, 100); System.out.println("原始的对象 = " + c1); DouBi c2 = null; // 通过序列化实现深拷贝 ByteArrayOutputStream bos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(bos); // 序列化以及传递这个对象 oos.writeObject(c1); oos.flush(); ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray()); ois = new ObjectInputStream(bin); // 返回新的对象 c2 = (DouBi) ois.readObject(); // 校验内容是否相同 System.out.println("复制后的对象 = " + c2); // 改变原始对象的内容 c1.setX(200); c1.setY(200); // 查看每一个现在的内容 System.out.println("查看原始的对象 = " + c1); System.out.println("查看复制的对象 = " + c2); } catch (IOException e) { System.out.println("Exception in main = " + e); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { if (oos != null) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } if (ois != null) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } }
- 输出结果如下:
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始的对象 = x=100, y=100 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 复制后的对象 = x=100, y=100 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 查看原始的对象 = x=200, y=200 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 查看复制的对象 = x=100, y=100
- 注意:需要做以下几件事儿:
- 确保对象图中的所有类都是可序列化的
- 创建输入输出流
- 使用这个输入输出流来创建对象输入和对象输出流
- 将你想要拷贝的对象传递给对象输出流
- 从对象输入流中读取新的对象并且转换回你所发送的对象的类
- 得出的结论
- 在这个例子中,创建了一个DouBi对象c1然后将它序列化 (将它写到ByteArrayOutputStream中). 然后我反序列化这个序列化后的对象并将它保存到c2中。随后我修改了原始对象c1。然后结果如你所见,c1不同于c2,对c1所做的任何修改都不会影响c2。
- 注意,序列化这种方式有其自身的限制和问题:因为无法序列化transient变量, 使用这种方法将无法拷贝transient变量。再就是性能问题。创建一个socket, 序列化一个对象, 通过socket传输它, 然后反序列化它,这个过程与调用已有对象的方法相比是很慢的。所以在性能上会有天壤之别。如果性能对你的代码来说是至关重要的,建议不要使用这种方式。它比通过实现Clonable接口这种方式来进行深拷贝几乎多花100倍的时间。
05.延迟拷贝
- 延迟拷贝是浅拷贝和深拷贝的一个组合,实际上很少会使用。这个以前几乎都没听说过,后来看书才知道有这么一种拷贝!
- 当最开始拷贝一个对象时,会使用速度较快的浅拷贝,还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时,它会决定数据是否被共享(通过检查计数器)并根据需要进行深拷贝。
- 延迟拷贝从外面看起来就是深拷贝,但是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可以使用延迟拷贝。由于存在计数器,效率下降很高,但只是常量级的开销。而且, 在某些情况下, 循环引用会导致一些问题。
06.如何选择拷贝方式
- 如果对象的属性全是基本类型的,那么可以使用浅拷贝。
- 如果对象有引用属性,那就要基于具体的需求来选择浅拷贝还是深拷贝。
- 意思是如果对象引用任何时候都不会被改变,那么没必要使用深拷贝,只需要使用浅拷贝就行了。如果对象引用经常改变,那么就要使用深拷贝。没有一成不变的规则,一切都取决于具体需求。
07.数组的拷贝
- 数组除了默认实现了clone()方法之外,还提供了Arrays.copyOf方法用于拷贝,这两者都是浅拷贝。
7.1 基本数据类型数组
- 如下所示
public void test4() { int[] lNumbers1 = new int[5]; int[] rNumbers1 = Arrays.copyOf(lNumbers1, lNumbers1.length); rNumbers1[0] = 1; boolean first = lNumbers1[0] == rNumbers1[0]; Log.d("小杨逗比", "lNumbers2[0]=" + lNumbers1[0] + ",rNumbers2[0]=" + rNumbers1[0]+"---"+first); int[] lNumbers3 = new int[5]; int[] rNumbers3 = lNumbers3.clone(); rNumbers3[0] = 1; boolean second = lNumbers3[0] == rNumbers3[0]; Log.d("小杨逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"---"+second); }
- 打印结果如下所示
2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/小杨逗比: lNumbers2[0]=0,rNumbers2[0]=1---false 2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/小杨逗比: lNumbers3[0]=0,rNumbers3[0]=1---false
7.2 引用数据类型数组
- 如下所示
public static void test5() { People[] lNumbers1 = new People[5]; lNumbers1[0] = new People(); People[] rNumbers1 = lNumbers1; boolean first = lNumbers1[0].equals(rNumbers1[0]); Log.d("小杨逗比", "lNumbers1[0]=" + lNumbers1[0] + ",rNumbers1[0]=" + rNumbers1[0]+"--"+first); People[] lNumbers2 = new People[5]; lNumbers2[0] = new People(); People[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length); boolean second = lNumbers2[0].equals(rNumbers2[0]); Log.d("小杨逗比", "lNumbers2[0]=" + lNumbers2[0] + ",rNumbers2[0]=" + rNumbers2[0]+"--"+second); People[] lNumbers3 = new People[5]; lNumbers3[0] = new People(); People[] rNumbers3 = lNumbers3.clone(); boolean third = lNumbers3[0].equals(rNumbers3[0]); Log.d("小杨逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"--"+third); } public static class People implements Cloneable { int age; Holder holder; @Override protected Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public static class Holder { int holderValue; } }
- 打印日志如下
2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小杨逗比: lNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18,rNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18--true 2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小杨逗比: lNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671,rNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671--true 2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小杨逗比: lNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56,rNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56--true
08.集合的拷贝
集合的拷贝也是我们平时经常会遇到的,一般情况下,我们都是用浅拷贝来实现,即通过构造函数或者clone方法。
8.1 集合浅拷贝
- 构造函数和 clone() 默认都是浅拷贝
public static void test6() { ArrayList<People> lPeoples = new ArrayList<>(); People people1 = new People(); lPeoples.add(people1); Log.d("小杨逗比", "lPeoples[0]=" + lPeoples.get(0)); ArrayList<People> rPeoples = (ArrayList<People>) lPeoples.clone(); Log.d("小杨逗比", "rPeoples[0]=" + rPeoples.get(0)); boolean b = lPeoples.get(0).equals(rPeoples.get(0)); Log.d("小杨逗比", "比较两个对象" + b); } public static class People implements Cloneable { int age; Holder holder; @Override protected Object clone() { try { People people = (People) super.clone(); people.holder = (People.Holder) this.holder.clone(); return people; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public static class Holder implements Cloneable { int holderValue; @Override protected Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } } }
- 打印日志
2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18 2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18 2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小杨逗比: 比较两个对象true
8.2 集合深拷贝
- 在某些特殊情况下,如果需要实现集合的深拷贝,那就要创建一个新的集合,然后通过深拷贝原先集合中的每个元素,将这些元素加入到新的集合当中。
public static void test7() { ArrayList<People> lPeoples = new ArrayList<>(); People people1 = new People(); people1.holder = new People.Holder(); lPeoples.add(people1); Log.d("小杨逗比", "lPeoples[0]=" + lPeoples.get(0)); ArrayList<People> rPeoples = new ArrayList<>(); for (People people : lPeoples) { rPeoples.add((People) people.clone()); } Log.d("小杨逗比", "rPeoples[0]=" + rPeoples.get(0)); boolean b = lPeoples.get(0).equals(rPeoples.get(0)); Log.d("小杨逗比", "比较两个对象" + b); } public static class People implements Cloneable { int age; Holder holder; @Override protected Object clone() { try { People people = (People) super.clone(); people.holder = (People.Holder) this.holder.clone(); return people; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public static class Holder implements Cloneable { int holderValue; @Override protected Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } } }
- 打印日志
2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18 2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671 2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小杨逗比: 比较两个对象false