`

【2016面试】java深复制和浅复制

 
阅读更多

我们在编码过程经常会碰到将一个对象传递给另一个对象,java中对于基本型变量采用的是值传递,而对于对象比如bean传递时采用的引用传递也就是地址传递,而很多时候对于对象传递我们也希望能够象值传递一样,使得传递之前和之后有不同的内存地址,在这种情况下我们一般采用以下两种情况。

浅复制与深复制概念

浅复制(浅克隆) :被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。

深复制(深克隆) :被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。

Java的clone()方法

(1)clone方法将对象复制了一份并返回给调用者。一般而言,clone()方法满足: 
①对任何的对象x,都有x.clone() !=x//克隆对象与原对象不是同一个对象; 
②对任何的对象x,都有x.clone().getClass()= =x.getClass()//克隆对象与原对象的类型一样 ; 
③如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立;

(2)Java中对象的克隆: 
①为了获取对象的一份拷贝,我们可以利用Object类的clone()方法。 
②在派生类中覆盖基类的clone()方法,并声明为public。 
③在派生类的clone()方法中,调用super.clone()。 
④在派生类中实现Cloneable接口。

package com.king.cloneable;

/**
 * 浅复制
 */
public class ShallowStudent implements Cloneable {
    private String name;

    private int age;

    ShallowStudent(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Object clone() {
        ShallowStudent o = null;
        try {
            // Object中的clone()识别出你要复制的是哪一个对象。
            o = (ShallowStudent) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        return o;
    }

    public static void main(String[] args) {
        ShallowStudent s1 = new ShallowStudent("zhangsan", 18);
        ShallowStudent s2 = (ShallowStudent) s1.clone();
        s2.name = "lisi";
        s2.age = 20;
        //修改学生2后,不影响学生1的值。
        System.out.println("name=" + s1.name + "," + "age=" + s1.age);
        System.out.println("name=" + s2.name + "," + "age=" + s2.age);
    }
}

Java的所有类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()。 JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用 new操作符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。

上面代码中有三个值得注意的地方,一是希望能实现clone功能的CloneClass类实现了Cloneable接口,这个接口属于java.lang包, java.lang包已经被缺省的导入类中,所以不需要写成java.lang.Cloneable。另一个值得请注意的是重载了clone()方法。最后在clone()方法中调用了super.clone(),这也意味着无论clone类的继承结构是什么样的,super.clone()直接或间接调用了java.lang.Object类的clone()方法。

下面再详细的解释一下这几点: 
应该说第三点是最重要的,仔细观察一下Object类的clone()一个native方法,native方法的效率一般来说都是远高于 java中的非native方法。这也解释了为什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息赋到新对象中,虽然这也实现了clone功能。对于第二点,也要观察Object类中的clone()还是一个protected属性的方法。这也意味着如果要应用 clone()方法,必须继承Object类,在Java中所有的类是缺省继承Object类的,也就不用关心这点了。然后重载clone()方法。还有一点要考虑的是为了让其它类能调用这个clone类的clone()方法,重载之后要把clone()方法的属性设置为public。

那么clone类为什么还要实现Cloneable接口呢?稍微注意一下,Cloneable接口是不包含任何方法的!其实这个接口仅仅是一个标志,而且这个标志也仅仅是针对Object类中clone()方法的,如果clone类没有实现Cloneable接口,并调用了Object的 clone()方法(也就是调用了super.Clone()方法),那么Object的clone()方法就会抛出 CloneNotSupportedException异常。

说明: 
①为什么我们在派生类中覆盖Object的clone()方法时,一定要调用super.clone()呢?在运行时刻,Object中的clone()识别出你要复制的是哪一个对象,然后为此对象分配空间,并进行对象的复制,将原始对象的内容一一复制到新对象的存储空间中。 
②继承自java.lang.Object类的clone()方法是浅复制。以下代码可以证明之。

package com.king.cloneable;

/**
 * 浅复制2
 */
public class ShallowStudent2 implements Cloneable {
    String name;// 常量对象。
    int age;
    Professor p;// 学生1和学生2的引用值都是一样的。

    ShallowStudent2(String name, int age, Professor p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }

    public Object clone() {
        ShallowStudent2 o = null;
        try {
            o = (ShallowStudent2) super.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        return o;
    }

    public static void main(String[] args) {
        Professor p = new Professor("wangwu", 50);
        ShallowStudent2 s1 = new ShallowStudent2("zhangsan", 18, p);
        ShallowStudent2 s2 = (ShallowStudent2) s1.clone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age);
        System.out.println("name=" + s2.p.name + "," + "age=" + s2.p.age);
        //输出结果学生1和2的教授成为lisi,age为30。
    }
}

class Professor {
    String name;
    int age;

    Professor(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

从中可以看出,调用Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象一样的空间,然后原样拷贝原始对象中的内容。对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。大多时候,这种clone的结果往往不是我们所希望的结果,这种clone也被称为"影子clone”。

那应该如何实现深层次的克隆,即修改s2的教授不会影响s1的教授?代码改进如下。 改进使学生1的Professor不改变(深层次的克隆):

package com.king.cloneable;

/**
 * 深复制
 */
public class DeepStudent implements Cloneable {
    String name;// 常量对象。
    int age;
    DeepProfessor p;// 学生1和学生2的引用值都是一样的。

    DeepStudent(String name, int age, DeepProfessor p) {
        this.name = name;
        this.age = age;
        this.p = p;
    }

    public Object clone() {
        DeepStudent o = null;
        try {
            o = (DeepStudent) super.clone();
            //对引用的对象也进行复制
            o.p = (DeepProfessor) p.clone();
        } catch (CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        return o;
    }

    public static void main(String[] args) {
        DeepProfessor p = new DeepProfessor("wangwu", 50);
        DeepStudent s1 = new DeepStudent("zhangsan", 18, p);
        DeepStudent s2 = (DeepStudent) s1.clone();
        s2.p.name = "lisi";
        s2.p.age = 30;
        System.out.println("name=" + s1.p.name + "," + "age=" + s1.p.age);
        System.out.println("name=" + s2.p.name + "," + "age=" + s2.p.age);
        //输出结果学生1和2的教授成为lisi,age为30。
    }
}

class DeepProfessor implements Cloneable {
    String name;
    int age;

    DeepProfessor(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Object clone() {
        DeepProfessor o = null;
        try {
            o = (DeepProfessor)super.clone();
        } catch(CloneNotSupportedException e) {
            System.out.println(e.toString());
        }
        return o;
    }
}

JDK中StringBuffer类型,关于StringBuffer的说明,StringBuffer没有重载clone()方法,更为严重的是StringBuffer还是一个 final类,这也是说我们也不能用继承的办法间接实现StringBuffer的clone。如果一个类中包含有StringBuffer类型对象或和 StringBuffer相似类的对象,我们有两种选择:要么只能实现影子clone,要么就在类的clone()方法中加一句(假设是 SringBuffer对象,而且变量名仍是p): o.p = new StringBuffer(p.toString()); //原来的是:o.p = (DeepProfessor) p.clone();

还要知道的是除了基本数据类型能自动实现深度clone以外,String对象是一个例外,它clone后的表现好象也实现了深度clone,虽然这只是一个假象,但却大大方便了我们的编程。

通过以上我们可以看出在某些情况下,我们可以利用clone方法来实现对象的深度复制,但对于比较复杂的对象(比如对象中包含其他对象,其他对象又包含别的对象…..)这样我们必须进行层层深度clone,每个对象需要实现cloneable接口,比较麻烦,那就继续学习下一个序列化方法。

利用串行化来做深复制

所谓对象序列化就是将对象的状态转换成字节流,以后可以通过这些值再生成相同状态的对象。这个过程也可以通过网络实现,可以先在Windows机器上创建一个对象,对其序列化,然后通过网络发给一台Unix机器,然后在那里准确无误地重新?装配?。是不是很神奇。

也许你会说,只了解一点点,但从来没有接触过,其实未必如此。RMI、Socket、JMS、EJB你总该用过一种吧,彼此为什么能够传递Java对象,当然都是对象序列化机制的功劳。 
第一次使用Java的对象序列化是做某项目,当时要求把几棵非常复杂的树(JTree)及相应的数据保存下来(就是我们常用的保存功能),以便下次运行程序时可以继续上次的操作。 
那时XML技术在网上非常的热,而且功能也强大,再加上树的结构本来就和XML存储数据的格式很像。作为一项对新技术比较有兴趣的我当然很想尝试一下。不过经过仔细分析,发现如果采用XML保存数据,后果真是难以想象:哪棵树的哪个节点被展开、展开到第几级、节点当前的属性是什么。真是不知该用A、B、C还是用1、2、3来表示。

还好,发现了Java的对象序列化机制,问题迎刃而解,只需简单的将每棵树的根节点序列化保存到硬盘上,下次再通过反序列化后的根节点就可以轻松的构造出和原来一模一样的树来。

其实保存数据,尤其是复杂数据的保存正是对象序列化的典型应用。最近另一个项目就遇到了需要对非常复杂的数据进行存取,通过使用对象的序列化,问题同样化难为简。

对象的序列化还有另一个容易被大家忽略的功能就是对象复制(Clone),Java中通过Clone机制可以复制大部分的对象,但是众所周知,Clone有深层Clone和浅层Clone,如果你的对象非常非常复杂,假设有个100层的Collection(夸张了点),如果你想实现深层 Clone,真是不敢想象,如果使用序列化,不会超过10行代码就可以解决。

主要是为了避免重写比较复杂对象的深复制的clone()方法,也可以程序实现断点续传等等功能。把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做 “解冻”或者“回鲜(depicking)”过程。

应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此“腌成咸菜”的只是对象的一个拷贝,Java咸菜还可以回鲜。

在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里(腌成咸菜),再从流里读出来(把咸菜回鲜),便可以重建对象。

如下为深复制源代码:

public Object deepClone() {    
   //将对象写到流里    
   ByteArrayOutoutStream bo=new ByteArrayOutputStream();    
   ObjectOutputStream oo=new ObjectOutputStream(bo);    
   oo.writeObject(this);    
   //从流里读出来    
   ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());    
   ObjectInputStream oi=new ObjectInputStream(bi);    
   return(oi.readObject());    
}

这样做的前提是对象以及对象内部所有引用到的对象都是可串行化的,否则,就需要仔细考察那些不可串行化的对象或属性可否设成transient,从而将之排除在复制过程之外。上例代码改进如下。

package com.king.cloneable;

import java.io.*;

/**
 * 通过串行化实现深复制
 */
class Teacher implements Serializable {
    String name;
    int age;

    public Teacher(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class DeepStudent2 implements Serializable {
    String name;//常量对象
    int age;
    Teacher t;//学生1和学生2的引用值都是一样的。

    public DeepStudent2(String name, int age, Teacher t) {
        this.name = name;
        this.age = age;
        this.t = t;
    }

    public Object deepClone() throws IOException, ClassNotFoundException {//将对象写到流里
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        ObjectOutputStream oo = new ObjectOutputStream(bo);
        oo.writeObject(this);//从流里读出来
        ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
        ObjectInputStream oi = new ObjectInputStream(bi);
        return (oi.readObject());
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Teacher t = new Teacher("tangliang", 30);
        DeepStudent2 s1 = new DeepStudent2("zhangsan", 18, t);
        DeepStudent2 s2 = (DeepStudent2) s1.deepClone();
        s2.t.name = "tony";
        s2.t.age = 40;
        //学生1的老师不改变
        System.out.println("name=" + s1.t.name + "," + "age=" + s1.t.age);
    }
}

虽然Java的序列化非常简单、强大,但是要用好,还有很多地方需要注意。比如曾经序列化了一个对象,可由于某种原因,该类做了一点点改动,然后重新被编译,那么这时反序列化刚才的对象,将会出现异常。

 

你可以通过添加serialVersionUID属性来解决这个问题。如果你的类是个单态(Singleton)类,是否允许用户通过序列化机制复制该类,如果不允许你需要谨慎对待该类的实现。

分享到:
评论

相关推荐

    Java后端面试问题整理.docx

    Java后端面试知识点总结,涉及JVM • 熟悉JVM内存区域,常用引用类型,垃圾回收机制、算法以及常见的GC垃圾收集器(Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1) • 熟悉常用IO模型(BIO、...

    JAVA核心面试知识pdf版

    JAVA核心面试知识pdf版,可以随意复制内容

    计算机Java、网络面试资料

    华为Java面试题集第一部分:基础知识 基础知识: 1.C++或Java中的异常处理机制的简单原理和应用。 当JAVA程序违反了JAVA的语义规则时,JAVA虚拟机就会将发生的错误表示为一个异常。违反语义规则包括2种情况。一种是...

    java核心面试技术点

    java核心面试技术点 线程 单例模式下的线程安全http://blog.sina.com.cn/s/blog_75247c770100yxpb.html map集合 集合数据结构及方法的使用 多线程 线程进程 线程状态 线程状态的改变 sql :临时表、游标、存贮...

    Java最常见200面试题以及解析

    细心整理,希望大家喜欢,简洁明了的回答面试的问题。总共包含十九个模块,分别是: Java 基础、容器、多线程、反射、对象拷 贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、...

    Java面试 Java超级经典100问题 Java高级开发工程师必备 Java面试宝典

    Java面试 Java超级经典100问题 Java高级开发工程师必备 Java面试宝典 1.赋值运算函数.2.单例设计模式.3.二维数组中查找目标值、4.替换字符串中的空格。5.从尾到头打印链表.6.由前序和中序遍历重建二叉树.7.用两个栈...

    JAVA面试题集合面试技能大全

    JAVA面试题集合面试技能大全 JAVA面试题集 基础知识: 1.C++或Java中的异常处理机制的简单原理和应用。 当JAVA程序违反了JAVA的语义规则时,JAVA虚拟机就会将发生的错误表示为一个异常。违反语义规则包括2种情况...

    java基础面试题复杂链表的复制

    java基础面试题复杂链表的复制本资源系百度网盘分享地址

    2018年最全Java面试通关秘籍第四套

    未经允许禁止一切形式的转载和复制,如有违反则追究其法律责任! 首先,我很懂你!当你看到这篇文章的时候可能是又喜又悲!喜的是,别人没看到这些悉心整理的面试题你看到了;悲的是,面试你的面试官可能也看到了!...

    Java面试宝典-经典

    2、编写一个程序,将d:\java目录下的所有.java文件复制到d:\jad目录下,并将原来文件的扩展名从.java改为.jad。 62 3、编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串,但要保证...

    Java面试宝典2010版

    2、编写一个程序,将d:\java目录下的所有.java文件复制到d:\jad目录下,并将原来文件的扩展名从.java改为.jad。 62 3、编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串,但要保证...

    java核心面试

    java并发面试题 .内存存储锁控制 http://blog.csdn.net/geolo/article/details/8670900 4.请说明下java的内存模型及其工作流程。 答:Java把内存划分成两种:一种是栈内存,一种是堆内存。 栈内存:存放对象:函数...

    Java开发面试-Redis专区

    当准备面试Java开发Redis部分时,对于工作一年左右经验的开发者来说,了解和掌握Redis的使用和应用是非常重要的。下面将为你详细介绍一些可能涉及的面试题,帮助你更好地准备。首先,Redis是一个开源的内存数据存储...

    Java面试宝典2016版-无书签高清版

    Java面试宝典2016版-无书签高清版,这个是附有答案的高清版,可以复制文字,但是没有书签

    Java 面试宝典2017版Linux公社高清版

    Java 面试宝典2017版Linux公社高清版,里面的题目都附有答案哦,文字可以复制的。

    Java面试宝典2013版pdf

    Java面试宝典pdf,面试必备,学习Java重点,回顾基础知识。

    Java面试宝典2020修订版V1.0.1.doc

    12、编写一个程序,将d:\java目录下的所有.java文件复制到d:\jad目录下,并将原来文件的扩展名从.java改为.jad。 60 13、java中有几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类...

    JAVA核心面试知识整理【书签完整】【高清可复制】

    内容包括jvm、java多线程、spring、微服务、netty、kafka、rabbimq、数据库、设计模式等等,你想掌握的以及你没有掌握的都可以作为应试参考资料

    Java初级开发工程师面试题汇总-附答案.PDF

    Java初级开发工程师面试题汇总.PDF,有答案的版本,可以复制文字。

    java面试题

    Java 软件工程师面试资料大整合 1 Java 面霸 1 1. int 和 Integer 有什么区别? 8 2. String 和StringBuffer的区别 8 3. 运行时异常与一般异常有何异同? 8 4. 说出ArrayList,Vector,LinkedList的存储性能和特性 8 5...

Global site tag (gtag.js) - Google Analytics