Java 简述对象克隆(复制)

时间:2021-03-02 18:20:53

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

int apples = 5;  
int pears = apples;

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

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

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

public class Student {
int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}

public class OuterMyTest {
public static void main(String[] args) {
Student stu1 = new Student();
stu1.setNumber(12345);  
Student stu2= stu1;
System.out.println(stu1.getNumber());
System.out.println(stu2.getNumber());
stu2.setNumber(54321);
System.out.println(stu1.getNumber());
System.out.println(stu2.getNumber());

                System.out.println(stu1.equals(stu2));

}
}

运行结果:

12345
12345
54321
54321

true


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

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

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

Java 简述对象克隆(复制)

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


是否记得万类之王Object。

它有11个方法,有两个protected的方法,其中一个为clone方法。

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

protected native Object clone() throws CloneNotSupportedException;

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

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

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

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


问:为什么要克隆?

答案是:克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。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为本地方法)

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

public class Student implements Cloneable{
int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
    public Object clone() {  
        Student stu = null;  
        try{  
            stu = (Student)super.clone();  
        }catch(CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        return stu;  
    }  

}


public class OuterMyTest {
public static void main(String[] args) {
Student stu1 = new Student();
stu1.setNumber(12345);  
//Student stu2 = stu1;
Student stu2 = (Student) stu1.clone();
System.out.println(stu1.getNumber());
System.out.println(stu2.getNumber());
stu2.setNumber(54321);
System.out.println(stu1.getNumber());
System.out.println(stu2.getNumber());

               System.out.println(stu1.equals(stu2));

}
}


运行结果:

12345
12345
12345
54321

false



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

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

public class Address {
String add;
public String getAdd() {
return add;
}
public void setAdd(String add) {
this.add = add;
}
}

public class Student implements Cloneable {
int number;
private Address addr;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public Object clone() {
Student stu = null;
try {
stu = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
public Address getAddr() {
return addr;
}
public void setAddr(Address addr) {
this.addr = addr;
}
}
public class OuterMyTest {


public static void main(String[] args) {
Address addr = new Address();
addr.setAdd("杭州市");
Student stu1 = new Student();
stu1.setNumber(12345);  
stu1.setAddr(addr);

Student stu2 = (Student) stu1.clone();
System.out.println(stu1.getNumber() + " " + stu1.getAddr().getAdd());
System.out.println(stu2.getNumber() + " " + stu2.getAddr().getAdd());

stu2.setNumber(54321);
addr.setAdd("衢州市");  
System.out.println(stu1.getNumber() + " " + stu1.getAddr().getAdd());
System.out.println(stu2.getNumber() + " " + stu2.getAddr().getAdd());
System.out.println(stu1.equals(stu2));

}

}


运行结果:

12345 杭州市
12345 杭州市
12345 衢州市
54321 衢州市
false


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

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

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

public class Address implements Cloneable{

String add;
public String getAdd() {
return add;
}
public void setAdd(String add) {
this.add = add;
}
public Object clone() {
Address add = null;
try {
add = (Address) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return add;
}
}


public class Student implements Cloneable {
int number;
private Address addr;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public Object clone() {
Student stu = null;
try {
stu = (Student) super.clone();   ////浅复制  
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
stu.addr = (Address) addr.clone();   //深度复制  
return stu;
}
public Address getAddr() {
return addr;
}
public void setAddr(Address addr) {
this.addr = addr;
}
}



public class OuterMyTest {


public static void main(String[] args) {
Address addr = new Address();
addr.setAdd("杭州市");
Student stu1 = new Student();
stu1.setNumber(12345);  
stu1.setAddr(addr);

Student stu2 = (Student) stu1.clone();
System.out.println(stu1.getNumber() + " " + stu1.getAddr().getAdd());
System.out.println(stu2.getNumber() + " " + stu2.getAddr().getAdd());

stu2.setNumber(54321);

addr.setAdd("衢州市");  
//stu2.getAddr().setAdd("衢州市");
System.out.println(stu1.getNumber() + " " + stu1.getAddr().getAdd());
System.out.println(stu2.getNumber() + " " + stu2.getAddr().getAdd());
System.out.println(stu1.equals(stu2));

}


}


运行结果:

12345 杭州市
12345 杭州市
12345 衢州市
54321 杭州市
false




浅克隆和深克隆

1、浅克隆

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

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

Java 简述对象克隆(复制)

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

2、深克隆

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

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

Java 简述对象克隆(复制)

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

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


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

回到顶部

解决多层克隆问题

如果引用类型里面还包含很多引用类型,或者内层引用类型的类里面又包含引用类型,使用clone方法就会很麻烦。

这时我们可以用序列化的方式来实现对象的深克隆。

Java 简述对象克隆(复制)
 1 public class Outer implements Serializable{
2 private static final long serialVersionUID = 369285298572941L; //最好是显式声明ID
3 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 }
Java 简述对象克隆(复制)

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

Java 简述对象克隆(复制)
 1 public class Inner implements Serializable{
2 private static final long serialVersionUID = 872390113109L; //最好是显式声明ID
3 public String name = "";
4
5 public Inner(String name) {
6 this.name = name;
7 }
8
9 @Override
10 public String toString() {
11 return "Inner的name值为:" + name;
12 }
13 }
Java 简述对象克隆(复制)

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



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


总结:

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

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

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

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



转载自:http://www.cnblogs.com/Qian123/p/5710533.html