深入理解Java中的克隆

前言

Java克隆(Clone)是Java语言的特性之一,但在实际中应用比较少见。但有时候用克隆会更方便更有效率。

对于克隆(Clone),Java有一些限制:

1、被克隆的类必须自己实现Cloneable 接口,以指示 Object.clone() 方法可以合法地对该类实例进行按字段复制。Cloneable 接口实际上是个标识接口,没有任何接口方法。

2、实现Cloneable接口的类应该使用公共方法重写 Object.clone(它是受保护的)。某个对象实现了此接口就克隆它是不可能的。即使 clone 方法是反射性调用的,也无法保证它将获得成功。

3、在Java.lang.Object类中克隆方法是这么定义的:

        protected Object clone()
        throws CloneNotSupportedException

创建并返回此对象的一个副本。表明是一个受保护的方法,同一个包中可见。

按照惯例,返回的对象应该通过调用 super.clone 获得。

Java中的赋值

在Java中,赋值是很常用的,一个简单的赋值如下

//原始类型
int a = 1;
int b = a;

//引用类型
String[] weekdays = new String[5];
String[] gongzuori = weekdays;//仅拷贝引用

在上述代码中。

1、如果是原始数据类型,赋值传递的为真实的值

2、如果是引用数据类型,赋值传递的为对象的引用,而不是对象。

了解了数据类型和引用类型的这个区别,便于我们了解clone。

Clone

在Java中,clone是将已有对象在内存中复制出另一个与之相同的对象的过程。java中的克隆为逐域复制。

在Java中想要支持clone方法,需要首先实现Cloneable接口

Cloneable其实是有点奇怪的,它不同与我们常用到的接口,它内部不包含任何方法,它仅仅是一个标记接口。

其源码如下

public interface Cloneable {
}

关于cloneable,需要注意的

1、如果想要支持clone,就需要实现Cloneable 接口

2、如果没有实现Cloneable接口的调用clone方法,会抛出CloneNotSupportedException异常。

然后是重写clone方法,并修改成public访问级别

static class CloneableImp implements Cloneable {
 public int count;
 public Child child;

 @Override
 public Object clone() throws CloneNotSupportedException {
   return super.clone();
 }
}

调用clone方法复制对象

CloneableImp imp1 = new CloneableImp();
imp1.child = new Child("Andy");
try {
 Object obj = imp1.clone();
 CloneableImp imp2 = (CloneableImp)obj;
 System.out.println("main imp2.child.name=" + imp2.child.name);
} catch (CloneNotSupportedException e) {
 e.printStackTrace();
}

浅拷贝

上面的代码实现的clone实际上是属于浅拷贝(Shallow Copy)。

关于浅拷贝,你该了解的

1、使用默认的clone方法

2、对于原始数据域进行值拷贝

3、对于引用类型仅拷贝引用

4、执行快,效率高

5、不能做到数据的100%分离。

6、如果一个对象只包含原始数据域或者不可变对象域,推荐使用浅拷贝。

关于无法做到数据分离,我们可以使用这段代码验证

CloneableImp imp1 = new CloneableImp();
imp1.child = new Child("Andy");
try {
 Object obj = imp1.clone();
 CloneableImp imp2 = (CloneableImp)obj;
 imp2.child.name = "Bob";

 System.out.println("main imp1.child.name=" + imp1.child.name);
} catch (CloneNotSupportedException e) {
 e.printStackTrace();
}

上述代码我们使用了imp1的clone方法克隆出imp2,然后修改 imp2.child.name 为 Bob,然后打印imp1.child.name 得到的结果是

main imp1.child.name=Bob

原因是浅拷贝并没有做到数据的100%分离,imp1和imp2共享同一个Child对象,所以一个修改会影响到另一个。

深拷贝

深拷贝可以解决数据100%分离的问题。只需要对上面代码进行一些修改即可。

1、Child实现Cloneable接口。

public class Child implements Cloneable{

 public String name;

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

 @Override
 public String toString() {
   return "Child [name=" + name + "]";
 }

 @Override
 protected Object clone() throws CloneNotSupportedException {
   return super.clone();
 }
}

2.重写clone方法,调用数据域的clone方法。

static class CloneableImp implements Cloneable {
 public int count;
 public Child child;

 @Override
 public Object clone() throws CloneNotSupportedException {
   CloneableImp obj = (CloneableImp)super.clone();
   obj.child = (Child) child.clone();
   return obj;
 }
}

当我们再次修改imp2.child.name就不会影响到imp1.child.name的值了,因为imp1和imp2各自拥有自己的child对象,因为做到了数据的100%隔离。

关于深拷贝的一些特点

1、需要重写clone方法,不仅仅只调用父类的方法,还需调用属性的clone方法

2、做到了原对象与克隆对象之间100%数据分离

3、如果是对象存在引用类型的属性,建议使用深拷贝

4、深拷贝比浅拷贝要更加耗时,效率更低

为什么使用克隆

很重要并且常见的常见就是:某个API需要提供一个List集合,但是又不希望调用者的修改影响到自身的变化,因此需要克隆一份对象,以此达到数据隔离的目的。

应尽量避免clone

1.通常情况下,实现接口是为了表明类可以为它的客户做些什么,而Cloneable仅仅是一个标记接口,而且还改变了超类中的手保护的方法的行为,是接口的一种极端非典型的用法,不值得效仿。

2.Clone方法约定及其脆弱 clone方法的Javadoc描述有点暧昧模糊,如下为 Java SE8的约定

clone方法创建并返回该对象的一个拷贝。而拷贝的精确含义取决于该对象的类。一般的含义是,对于任何对象x,表达式

x.clone() != x 为 true x.clone().getClass() == x.getClass() 也返回true,但非必须 x.clone().equals(x) 也返回true,但也不是必须的

上面的第二个和第三个表达式很容易就返回false。因而唯一能保证永久为true的就是表达式一,即两个对象为独立的对象。

3.可变对象final域 在克隆方法中,如果我们需要对可变对象的final域也进行拷贝,由于final的限制,所以实际上是无法编译通过的。因此为了实现克隆,我们需要考虑舍去该可变对象域的final关键字。

4.线程安全 如果你决定用线程安全的类实现Cloneable接口,需要保证它的clone方法做好同步工作。默认的Object.clone方法是没有做同步的。

总的来说,java中的clone方法实际上并不是完善的,建议尽量避免使用。如下是一些替代方案。

Copy constructors

使用复制构造器也可以实现对象的拷贝。

1、复制构造器也是构造器的一种

2、只接受一个参数,参数类型为当前的类

3、目的是生成一个与参数相同的新对象

4、复制构造器相比clone方法的优势是简单,易于实现。

一段使用了复制构造器的代码示例

public class Car {
 Wheel wheel;
 String manufacturer;

 public Car(Wheel wheel, String manufacturer) {
   this.wheel = wheel;
   this.manufacturer = manufacturer;
 }

 //copy constructor
 public Car(Car car) {
   this(car.wheel, car.manufacturer);
 }

 public static class Wheel {
   String brand;
 }
}

注意,上面的代码实现为浅拷贝,如果想要实现深拷贝,参考如下代码

//copy constructor
public Car(Car car) {
 Wheel wheel = new Wheel();
 wheel.brand = car.wheel.brand;

 this.wheel = wheel;
 this.manufacturer = car.manufacturer;
}

为了更加便捷,我们还可以为上述类增加一个静态的方法

public static Car newInstance(Car car) {
 return new Car(car);
}

使用Serializable实现深拷贝

其实,使用序列化也可以实现对象的深拷贝。简略代码如下

public class DeepCopyExample implements Serializable{
 private static final long serialVersionUID = 6098694917984051357L;
 public Child child;

 public DeepCopyExample copy() {
   DeepCopyExample copy = null;
   try {
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
     ObjectOutputStream oos = new ObjectOutputStream(baos);
     oos.writeObject(this);

     ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
     ObjectInputStream ois = new ObjectInputStream(bais);
     copy = (DeepCopyExample) ois.readObject();
   } catch (IOException e) {
     e.printStackTrace();
   } catch (ClassNotFoundException e) {
     e.printStackTrace();
   }
   return copy;
 }
}

其中,Child必须实现Serializable接口

public class Child implements Serializable{
 private static final long serialVersionUID = 6832122780722711261L;
 public String name = "";

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

 @Override
 public String toString() {
   return "Child [name=" + name + "]";
 }
}

使用示例兼测试代码

DeepCopyExample example = new DeepCopyExample();
example.child = new Child("Example");

DeepCopyExample copy = example.copy();
if (copy != null) {
 copy.child.name = "Copied";
 System.out.println("example.child=" + example.child + ";copy.child=" + copy.child);
}
//输出结果:example.child=Child [name=Example];copy.child=Child [name=Copied]

由输出结果来看,copy对象的child值修改不影响example对象的child值,即使用序列化可以实现对象的深拷贝。

总结

以上就是Java中克隆的全部内容,希望本文对大家学习Java能有所帮助。

(0)

相关推荐

  • Java中对象的深复制(深克隆)和浅复制(浅克隆)介绍

    1.浅复制与深复制概念 ⑴浅复制(浅克隆) 被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象.换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象. ⑵深复制(深克隆) 被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量.那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象.换言之,深复制把要复制的对象所引用的对象都复制了一遍. 2.Java的clone()方法 ⑴clone方法将对象复制了一份并返回

  • Java中对象的序列化方式克隆详解

    Java 序列化技术可以使你将一个对象的状态写入一个Byte 流里,并且可以从其它地方把该Byte 流里的数据读出来,重新构造一个相同的对象. 简述: 用字节流的方式,复制Java对象 代码: 流克隆复制函数 public static Object deepClone(Object obj){ if(obj == null){ return null; } try { ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); Ob

  • 基于序列化存取实现java对象深度克隆的方法详解

    我们知道,在java中,将一个非原型类型类型的对象引用,赋值给另一个对象的引用之后,这两个引用就指向了同一个对象,如: 复制代码 代码如下: public class DeepCloneTest { private class CloneTest {  private Long myLong = new Long(1); } public static void main(String args[]) {  new DeepCloneTest().Test(); } public void Te

  • 浅谈Java中的克隆close()和赋值引用的区别

    学生类Student: package 克隆clone; /*要克隆必须实现这个借口:Cloneable,以标记这个对象可以克隆 Cloneable:此类实现了 Cloneable 接口,以指示 Object.clone() 方法可以合法地对该类实例进行按字段复制. 这个接口是标记接口,告诉我们实现该接口的类就可以实现对象的复制了. */ public class Student implements Cloneable { private String name; private int ag

  • java对象序列化与反序列化的默认格式和json格式使用示例

    默认格式 复制代码 代码如下: public class MyClass implements Serializable{...} 序列化: 复制代码 代码如下: ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream(outputPath)); output.writeObject(myObject); 反序列化: 复制代码 代码如下: ObjectInputStream input = new Objec

  • 深入JAVA对象深度克隆的详解

    有时候,我们需要把对象A的所有值复制给对象B(B = A),但是这样用等号给赋值你会发现,当B中的某个对象值改变时,同时也会修改到A中相应对象的值!也许你会说,用clone()不就行了?!你的想法只对了一半,因为用clone()时,除了基础数据和String类型的不受影响外,其他复杂类型(如集合.对象等)还是会受到影响的!除非你对每个对象里的复杂类型又进行了clone(),但是如果一个对象的层次非常深,那么clone()起来非常复杂,还有可能出现遗漏!既然用等号和clone()复制对象都会对原来

  • Java中对象序列化与反序列化详解

    本文实例讲述了Java中对象序列化与反序列化.分享给大家供大家参考.具体如下: 一.简介 对象序列化(Serializable)是指将对象转换为字节序列的过程,而反序列化则是根据字节序列恢复对象的过程. 序列化一般用于以下场景: 1.永久性保存对象,保存对象的字节序列到本地文件中: 2.通过序列化对象在网络中传递对象: 3.通过序列化在进程间传递对象. 对象所属的类必须实现Serializable或是Externalizable接口才能被序列化.对实现了Serializable接口的类,其序列化

  • 深入理解Java对象的序列化与反序列化的应用

    当两个进程在进行远程通信时,彼此可以发送各种类型的数据.无论是何种类型的数据,都会以二进制序列的形式在网络上传送.发送方需要把这个Java对象转换为字节序列,才能在网络上传送:接收方则需要把字节序列再恢复为Java对象. 把Java对象转换为字节序列的过程称为对象的序列化.把字节序列恢复为Java对象的过程称为对象的反序列化.对象的序列化主要有两种用途:1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中:2) 在网络上传送对象的字节序列.一. JDK类库中的序列化APIjava.io

  • 解析JAVA深度克隆与浅度克隆的区别详解

    在JAVA克隆对象不能简单的使用clone方法,clone方法只是进行浅克隆.请看下方:深度克隆类:Java代码 复制代码 代码如下: import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public cla

  • java 序列化对象 serializable 读写数据的实例

    序列化对象: 复制代码 代码如下: package com.chen.seriaizable; import java.io.Serializable;import java.util.List; @SuppressWarnings("serial")public class Student implements Serializable{ private String name; private String id; private int age; private List<

  • java序列化和java反序列化示例

    序列化一般应用与以下场景之中:1.永久性保存对象,把对象通过序列化字节流保存到本地文件中:2.通过序列化在网络中传输对象3.通过序列化在进程间传递对象 复制代码 代码如下: import java.io.Serializable;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream; public class javaSerializable_fun { /**  

随机推荐