码迷,mamicode.com
首页 > 编程语言 > 详细

Java对象克隆

时间:2017-10-12 22:52:27      阅读:224      评论:0      收藏:0      [点我收藏+]

标签:输出   java对象   应聘   protect   深度   tput   style   play   int   

一、前言

前两天,去应聘一家公司的笔试题,里面一道编程题目如下:

请问你如何理解Java中的对象克隆呢?

当时一脸懵逼,虽然秉承着不留空白的原则将那道题目写了一大堆,但是可惜结果是错误的。接下来我会将自己的错误贴在下面。读者别效仿笔者,闹出笑话。然后回来重新回炉了相关的知识,发现一位大神的博客解释的很到位。所以这里很多东西都是对该博主的内容进行转载,并且按照自己的理解加以修改的。

下面附上博主的链接:http://www.cnblogs.com/Qian123/p/5710533.html#_labelTop,毕竟人家也是辛辛苦苦整理的博文,因此在这里表示对该博主的感谢。

二、认识Java对象克隆

在java面向对象的编程当中,要复制引用类型的对象,就必须克隆这些对象。通过调用对所有引用类型和对象都是可用的clone方法,来实现克隆。如果是值类型的实例,那么“=”赋值运算符就可以将源对象的状态逐字节地复制到目标对象中。笔者理解:也就是对象克隆的话,那么就是说,我们将目前这个对象复制了一份,而被复制的那个对象其实没有改变,我们要操作的只是克隆得到的那个对象,而不会修改被复制的克隆对象。(PS:解释的有点乱,但笔者自己是能够理解,有更加通俗易懂的说法,请在评论区指教!)

三、关于对象克隆的认知错误

在说克隆对象之前,先来讲讲变量的复制:

int apples = 5;  
int pears = apples;  

以上就是变量的复制,并且不仅仅是int类型的可以复制,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。

但是对于对象的复制(克隆)的话,情况就有点复杂了,因为笔者之前也没有接触过,所以导致了认知上的一些错误。下面将自己认知错误贴在下面:

package com.ygh.cloneObj;

/**
 * 学生类
 * 
 * @author 夜孤寒
 * @version 1.1.1
 */
class Student {
    private int number;

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

}

/**
 * 测试类
 * 
 * @author Administrator
 * @version 1.1.1
 */
public class Test {

    public static void main(String args[]) {
        // 创建对象
        Student stu_1 = new Student();
        stu_1.setNumber(12345);
        // 对象引用赋值
        Student stu2 = stu_1;

        System.out.println("学生1:" + stu_1.getNumber());
        System.out.println("学生2:" + stu2.getNumber());
    }
}

以上代码的输出结果如下:

技术分享

可能这时候我们就会以为我们已经达到了克隆的目的了,但是其实不是这样的,我们用事实来讲述我们的错误,我们将stu_2的setNumber()方法设置为54321,贴出源代码:

package com.ygh.cloneObj;

/**
 * 学生类
 * 
 * @author 夜孤寒
 * @version 1.1.1
 */
class Student {
    private int number;

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

}

/**
 * 测试类
 * 
 * @author Administrator
 * @version 1.1.1
 */
public class Test {

    public static void main(String args[]) {
        // 创建对象
        Student stu_1 = new Student();
        stu_1.setNumber(54321);
        // 对象引用赋值
        Student stu2 = stu_1;

        System.out.println("学生1:" + stu_1.getNumber());
        System.out.println("学生2:" + stu2.getNumber());
    }
}

输出结果:

技术分享

这时候我们就会发现两者的结果都是54321。

 这时候我们就会发现,我们仅仅是设置了stu_2的值,但是stu_1的值也跟着改变了。这就是说我们并没有达到对象克隆的目的。下面我们从内存图来理解为什么会出现这种情况:

技术分享

 

我们会发现我们仅仅是将stu_1的引用赋值给stu_2引用而已,两者指向的其实还是同一个对象,也就是说我们根本没有达到对象克隆的目的。

那么如何能够复制一个对象呢?

我们可以记得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("学生1:" + stu1.getNumber());  
        System.out.println("学生2:" + stu2.getNumber());  
          
        stu2.setNumber(54321);  
      
        System.out.println("学生1:" + stu1.getNumber());  
        System.out.println("学生2:" + stu2.getNumber());  
    }  
}

这时候输出的结果为:

技术分享

这时候我们就可以发现对象已经被克隆了,两个不同的引用指向的不是同一个对象。如果还不相信的话,可以在代码的末尾加上一句代码:

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

这就说明了两个引用指向的不是同一个对象。

但是上面的代码仅仅只是实现了浅复制。下面介绍一下深度复制,在上述的那个类中再加上一个Address类:

class Address  {  
    private String add;  
  
    public String getAdd() {  
        return add;  
    }  
  
    public void setAdd(String add) {  
        this.add = add;  
    }  
      
}  
  
class Student implements Cloneable{  
    private int number;  
  
    private Address addr;  
      
    public Address getAddr() {  
        return addr;  
    }  
  
    public void setAddr(Address addr) {  
        this.addr = addr;  
    }  
  
    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[]) {  
          
        Address addr = new Address();  
        addr.setAdd("杭州市");  
        Student stu1 = new Student();  
        stu1.setNumber(123);  
        stu1.setAddr(addr);  
          
        Student stu2 = (Student)stu1.clone();  
          
        System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
    }  
}

结果:

技术分享

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

我们在main方法中添加以下代码:

addr.setAdd("西湖区");  
  
System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  

这时候我们就会发现输出结果与我们的想要的不一样了:

技术分享

我们所希望的是后两句的第一句地址是杭州市,第二句地址是西湖区。而之所以会出现这样的原因是:浅复制只是复制了addr变量的引用,并没有真正的开辟另一块空间,将值复制后再将引用返回给新对象。

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

技术分享
package abc;  
  
class Address implements Cloneable {  
    private String add;  
  
    public String getAdd() {  
        return add;  
    }  
  
    public void setAdd(String add) {  
        this.add = add;  
    }  
      
    @Override  
    public Object clone() {  
        Address addr = null;  
        try{  
            addr = (Address)super.clone();  
        }catch(CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        return addr;  
    }  
}  
  
class Student implements Cloneable{  
    private int number;  
  
    private Address addr;  
      
    public Address getAddr() {  
        return addr;  
    }  
  
    public void setAddr(Address addr) {  
        this.addr = addr;  
    }  
  
    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();  
        }  
        stu.addr = (Address)addr.clone();   //深度复制  
        return stu;  
    }  
}  
public class Test {  
      
    public static void main(String args[]) {  
          
        Address addr = new Address();  
        addr.setAdd("杭州市");  
        Student stu1 = new Student();  
        stu1.setNumber(123);  
        stu1.setAddr(addr);  
          
        Student stu2 = (Student)stu1.clone();  
          
        System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
          
        addr.setAdd("西湖区");  
          
        System.out.println("学生1:" + stu1.getNumber() + ",地址:" + stu1.getAddr().getAdd());  
        System.out.println("学生2:" + stu2.getNumber() + ",地址:" + stu2.getAddr().getAdd());  
    }  
}
View Code

输出结果:

技术分享

以上的结果就符合我们的想法了。

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

技术分享
/** 
 * 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;  
}
View Code

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

 六、浅克隆和深克隆

1、浅克隆

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

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

技术分享

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

2、深克隆

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

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

技术分享

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

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

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

七、知识扩展

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

八、多层克隆问题的解决

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

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

public class Inner implements Serializable{
  private static final long serialVersionUID = 872390113109L; //最好是显式声明ID
  public String name = "";

  public Inner(String name) {
      this.name = name;
  }

  @Override
  public String toString() {
      return "Inner的name值为:" + name;
  }
}

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

九、总结

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

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

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

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

 

Java对象克隆

标签:输出   java对象   应聘   protect   深度   tput   style   play   int   

原文地址:http://www.cnblogs.com/Java766357768/p/7658427.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!