重学Java(4):浅拷贝深拷贝

一、为什么要使用拷贝

    为了提升性能,当一个类比较大时,通过 new 新建对象的时候,是要花费巨大代价的。为了解决这个问题 Java 中提供了

    Cloneable 这个借口。实现这个借口的类就具备了被拷贝的能力,而拷贝是在内存中做的,比通过 new 新建对象速度更快。

    我们知道拷贝有深浅之分,而浅拷贝又有拷贝对象属性不彻底的问题。所以就有了下面的问题。

二、浅拷贝的问题

    1.浅拷贝,即使用一个已知实例对新建实例的成员变量逐个赋值。当拷贝基本数据类型时,两个实例的成员变量已有存储空    间,赋值运算传值。当拷贝引用型数据时,赋值时值传递引用。这样就会造成一个问题:当修改其中某一个对象引用型数据的  值时,其他的对象的引用型数据的值也修改了。例如下面的例子:
 1 public class Game {
 2
 3     private String game;
 4
 5     public Game(String game) {
 6         this.game = game;
 7     }
 8
 9     public String play(){
10         return game;
11     }
12
13     public void setGame(String game){
14         this.game = game;
15     }
16 }


 1 public class People implements Cloneable {
 2
 3     private String name;
 4
 5     private Game game;
 6
 7     public People(String name, Game game) {
 8         this.name = name;
 9         this.game = game;
10     }
11
12     public String getName() {
13         return name;
14     }
15
16     public void setName(String name) {
17         this.name = name;
18     }
19
20     public Game getGame() {
21         return game;
22     }
23
24     public void setGame(Game game) {
25         this.game = game;
26     }
27
28     @Override protected People clone(){
29         People people = null;
30         try {
31             people = (People) super.clone();
32         } catch (CloneNotSupportedException e) {
33             e.printStackTrace();
34         }
35         return people;
36     }
37 }
 1     public void testClone(){
 2         Game game = new Game("刺客信条");
 3
 4         People people1 = new People("1",game);
 5         People people2 = people1.clone();
 6         people2.setName("2");
 7         People people3 = people1.clone();
 8         people3.setName("3");
 9
10         System.out.println("name:"+people1.getName()+"game:" + people1.getGame().play());
11         System.out.println("name:"+people2.getName()+"game:" + people2.getGame().play());
12         System.out.println("name:"+people3.getName()+"game:" + people3.getGame().play());
13
14         people3.getGame().setGame("战神");
15
16         System.out.println("name:"+people1.getName()+"game:" + people1.getGame().play());
17         System.out.println("name:"+people2.getName()+"game:" + people2.getGame().play());
18         System.out.println("name:"+people3.getName()+"game:" + people3.getGame().play());
19     }
1 Output:
2
3 name:1game:刺客信条
4 name:2game:刺客信条
5 name:3game:刺客信条
6 name:1game:战神
7 name:2game:战神
8 name:3game:战神

    从上面的例子可以看出,浅拷贝确实只是传了引用,当改变引用型数据的值后,指向此数据的所有引用的值也发生了改变。问题其实就是出现在 clone 这个方法上。

clone 这个方法是调用父类的 clone 方法,super.clone() 即Object 的 clone 方法。但是这个 clone 方法是有缺陷的,它并不会拷贝对象的所有属性,而是有选择性的拷贝。

其规则如下:

  a.基本类型:如果变量是基本类型,则拷贝其值。例如:int、float等。

  b.对象:如果变量是对象,则拷贝其对象引用,也就是说此时原来的对象与新对象公用该实例变量。

  c.String 字符串:如果变量是字符串,则拷贝其地址引用。但是在修改时,会从字符串池中生成新的字符串。

  通过以上规则,可以看出由于people1、people2、people3 其实都是指向同一个引用,故修改了其中一个的游戏时,其他两个也被修改了。

  这时,修改 clone 方法就可以解决这个问题,如:

 1     @Override protected People clone(){
 2         People people = null;
 3         try {
 4             people = (People) super.clone();
 5             people.setGame(new Game(people.getGame().play()));
 6         } catch (CloneNotSupportedException e) {
 7             e.printStackTrace();
 8         }
 9         return people;
10     }

  但是这样解决也会有问题,当对象非常大时,这样做会非常的麻烦。于是就有了下面的解决方法。

二、利用序列化实现对象的拷贝

  方法:将原始对象写入到字节流当中,再从字节流当中将其读出来,这样就可以创建一个新的对象。而且新对象与原始对象并不存在引用共享的问题。具体方法

如下:

 1 public class CloneUtil {
 2
 3     public static <T extends Serializable> T clone(T object){
 4         T cloneObj = null;
 5         ByteArrayOutputStream bos = null;
 6         ObjectOutputStream oos = null;
 7         ByteArrayInputStream bis = null;
 8         ObjectInputStream ois = null;
 9         try {
10             bos = new ByteArrayOutputStream();
11             oos = new ObjectOutputStream(bos);
12             oos.writeObject(object);
13
14             bis = new ByteArrayInputStream(bos.toByteArray());
15             ois = new ObjectInputStream(bis);
16             cloneObj = (T) ois.readObject();
17         }catch (Exception e){
18             e.printStackTrace();
19         }finally {
20             if (null != ois){
21                 try {
22                     ois.close();
23                 } catch (IOException e) {
24                     e.printStackTrace();
25                 }
26             }
27             if (null != bis){
28                 try {
29                     bis.close();
30                 } catch (IOException e) {
31                     e.printStackTrace();
32                 }
33             }
34             if (null != oos){
35                 try {
36                     oos.close();
37                 } catch (IOException e) {
38                     e.printStackTrace();
39                 }
40             }
41             if (null != bos){
42                 try {
43                     bos.close();
44                 } catch (IOException e) {
45                     e.printStackTrace();
46                 }
47             }
48         }
49         return cloneObj;
50     }
51 }

  使用上述方法工具类的对象需要实现 Serializable 接口,无需实现 Cloneable 接口的 clone() 方法。

  

 
时间: 2024-10-14 14:23:57

重学Java(4):浅拷贝深拷贝的相关文章

重学Java(一):与《Java编程思想》的不解之缘

说起来非常惭愧,我在 2008 年的时候就接触了 Java,但一直到现在(2018 年 10 月 10 日),基础知识依然非常薄弱.用一句话自嘲就是:十年 IT 老兵,Java 菜鸡一枚. 于是,我想,不如静下心来,重新读一遍那些经典的 Java 技术书,并且没读完一章就输出一篇原创技术文章.从哪一本开始呢?想了一想,还是从<Java 编程思想>开始吧!毕竟这本书赢得了全球程序员的广泛赞誉,从 Java 的基础语法到最高级特性,都能指导我们 Java 程序员轻松掌握. 记得刚上大学那会,就买了

重学Java(3):抽象类与接口

一.抽象类 1.抽象类不能被实例化,实例化要交给子类完成,它只需要一个子类就可以完成 2.抽象方法必须由子类来进行重写 3.如果一个类包含一个抽象方法,则该类必须定义成抽象类 4.抽象类中可以包含具体方法,也可以不包含 5.子类中的抽象方法不能与父类重名 6.abstract 不能与 final 同时修饰一个类 7.abstract 不能与 private.static.final 或者 native 并列修饰同一个方法 二.接口 接口是一种比抽象类更加抽象的“类”,接口本身并不是类.它不能直接

重学JAVA基础(八):锁的基本知识

1.线程状态 如上图,当我们新建一个线程,并start后,其实不一定会马上执行,因为只有操作系统调度了我们的线程,才能真正进行执行,而操作系统也随时可以运行其他线程,这时线程又回到可运行状态.这个过程是操作系统控制的,不是我们能控制的.我们能控制的是将线程变为blocked,并从blocked变为runable状态. 在以前实验的wait和notify中,我当时并没有注意到notify后会进行锁阶段,以为notify后直接进入runable状态,等待操作系统调度.  从上面的图中可以看到,我们n

重学JAVA基础(一):PATH和CLASSPATH

我想大多数Java初学者都会遇到的问题,那就是怎么配置环境,执行java -jar xxx.jar  都会报NoClassDefFindError,我在最开始学习的时候,也遇到了这些问题. 1.PATH path是路径的意思,我们直接在控制台运行一个程序时候,系统都会在指定的path下去找有没有这个程序,如果有就执行,没有就报错或者提示. 在windows系统中,使用echo %PATH%来查看PATH下的路径 在linux系统中,使用 echo $PATH 我们将java的bin目录加入pat

重学Java(2):Java数字的处理

一.简单的例子 1 public void testNum(){ 2 System.out.println(Math.round(12.5)); 3 System.out.println(Math.round(-12.5)); 4 } 5 6 Output: 13 7 -12 输出是由 Math.round 的四舍五入规则影响的. 二.Java中的舍入法 1.ROUND_UP:远离零方向舍入.向绝对值最大的方向舍入,只要舍弃位非0即进位. 2.ROUND_DOWN:趋向零方向舍入.向绝对值最小的

重学JAVA基础(五):面向对象

1.封装 import java.util.Date; public class Human { protected String name; protected BirthDay birthDay; protected String sex; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return

【Java】 重学Java的一些笔记

1. 一个Java程序中只能包含类,类中只能包含变量和方法,方法中只能包含变量和语句.也就是我们不能把语句(比如for语句)写在类中. 2. 一定注意=与==之间的区别,尤其在对boolean进行if判断的过程中,容易出现错误. 3. 强制类型转换只能用在基本数据类型之间,(String)c是不对的,应该先把c变成对象,再调用toString()方法,即new Character(c); c.toString;.

重学JAVA基础(二):Java反射

看一下百度的解释: JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息    以及动态调用对象的方法的功能称为java语言的反射机制. 先看一下一个例子: 这是最简单的反射使用方法,通过反射来调用类的方法. 下面通过一个需求来做反射实验:有3种人类(黄,白,黑),分别继承于Human类,都有人类的共同操作Behaviour /** * 行为,区别于动物 * @author tomsnail *

重学Java面向对象 之 final

final 的套路: 当初在背面试题的时候final出现的概率可以说是相当高了,在各种面试题库中都少不了它的身影,一说起final ,那打开方式差不多就是这样的: 1.  对于基本类型变量:final 修饰的变量不可修改 2.  对于引用型变量: final 修饰的对象,引用本身不可修改,但是被引用的内容可以修改. 3. 对于 方法 : 方法不能重写 4. 对于类:类不能被继承 因为当时看了太多遍同时内容简单又好背,现在不看书也能写出来了,至于具体的代码示例这里就不放了,网上也比较多. 但是自己