Java编程实现对象克隆(复制)代码详解

 更新时间:2017年11月29日 11:34:31   作者:萌小Q  
这篇文章主要介绍了Java编程实现对象克隆(复制)代码详解,涉及了克隆的原因,如何实现克隆,克隆的一般步骤,深克隆与浅克隆的介绍等相关内容,具有一定借鉴价值,需要的朋友可以参考下。

克隆,想必大家都有耳闻,世界上第一只克隆羊多莉就是利用细胞核移植技术将哺乳动物的成年体细胞培育出新个体,甚为神奇。其实在Java中也存在克隆的概念,即实现对象的复制。

本文将尝试介绍一些关于Java中的克隆和一些深入的问题,希望可以帮助大家更好地了解克隆。

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

int apples = 5; 
int pears = apples; 

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

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

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

class Student { 
  private int number; 
 
  public int getNumber() { 
    return number; 
  } 
 
  public void setNumber(int number) { 
    this.number = number; 
  } 
   
} 
public class Test { 
   
  public static void main(String args[]) { 
    Student stu1 = new Student(); 
    stu1.setNumber(12345); 
    Student stu2 = stu1; 
     
    System.out.println("学生1:" + stu1.getNumber()); 
    System.out.println("学生2:" + stu2.getNumber()); 
  } 
}

结果:

学生1:12345 
学生2:12345

这里我们自定义了一个学生类,该类只有一个number字段。

我们新建了一个学生实例,然后将该值赋值给stu2实例。(Student stu2 = stu1;)

再看看打印结果,作为一个新手,拍了拍胸腹,对象复制不过如此,

难道真的是这样吗?

我们试着改变stu2实例的number字段,再打印结果看看:

stu2.setNumber(54321); 
 
System.out.println("学生1:" + stu1.getNumber()); 
System.out.println("学生2:" + stu2.getNumber()); 

结果:

学生1:54321 
学生2:54321  

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

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

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

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

是否记得万类之王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虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。

第一次声明保证克隆对象将有单独的内存地址分配。
第二次声明表明,原始和克隆的对象应该具有相同的类类型,但它不是强制性的。
第三声明表明,原始和克隆的对象应该是平等的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());
	}
}

结果:

学生1:12345 
学生2:12345 
学生1:12345 
学生2:54321

如果你还不相信这两个对象不是同一个对象,那么你可以看看这一句:

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());
	}
}

结果:

学生1:123,地址:杭州市 
学生2:123,地址:杭州市 

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

我们在main方法中试着改变addr实例的地址。

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

结果:

学生1:123,地址:杭州市 
学生2:123,地址:杭州市 
学生1:123,地址:西湖区 
学生2:123,地址:西湖区 

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

原因是浅复制只是复制了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()); 
  } 
}

结果:

学生1:123,地址:杭州市
学生2:123,地址:杭州市
学生1:123,地址:西湖区
学生2:123,地址:杭州市

这样结果就符合我们的想法了。

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

java.util.Date:

/** 
 * 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; 
}

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

浅克隆和深克隆

1、浅克隆

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

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

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

2、深克隆

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

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

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

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

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

扩展

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

解决多层克隆问题

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

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编程实现对象克隆(复制)代码详解的全部内容,希望对大家有所帮助。感兴趣的来朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。

相关文章

  • SpringBoot开发实战之自动配置

    SpringBoot开发实战之自动配置

    SpringBoot的核心就是自动配置,自动配置又是基于条件判断来配置Bean,下面这篇文章主要给大家介绍了关于SpringBoot开发实战之自动配置的相关资料,需要的朋友可以参考下
    2021-08-08
  • RabbitMq报错reply-code=406 reply-text=PRECONDITION_FAILED解决

    RabbitMq报错reply-code=406 reply-text=PRECONDITION_FAILED

    这篇文章主要为大家介绍了RabbitMq报错reply-code=406 reply-text=PRECONDITION_FAILED分析解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • AsyncHttpClient IOExceptionFilter异常过滤器

    AsyncHttpClient IOExceptionFilter异常过滤器

    这篇文章主要为大家介绍了AsyncHttpClient IOExceptionFilter异常过滤器代码流程解读,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • Java Apollo是如何实现配置更新的

    Java Apollo是如何实现配置更新的

    这篇文章主要介绍了Java Apollo是如何实现配置更新的,帮助大家更好的理解和学习使用Java,感兴趣的朋友可以了解下
    2021-03-03
  • SpringBoot Security从入门到实战示例教程

    SpringBoot Security从入门到实战示例教程

    Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架,接下来通过本文给大家介绍SpringBoot Security从入门到实战示例教程,感兴趣的朋友一起看看吧
    2022-05-05
  • 再谈java回调函数

    再谈java回调函数

    个人对于回调函数的理解就是回调函数就是回头再调用的函数,哈哈,下面我们来详细探讨下回调函数。
    2015-07-07
  • 使用Redis incr解决并发问题的操作

    使用Redis incr解决并发问题的操作

    这篇文章主要介绍了使用Redis incr解决并发问题的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • SpringBoot中连接多个RabbitMQ的方法详解

    SpringBoot中连接多个RabbitMQ的方法详解

    这篇文章主要介绍了SpringBoot中连接多个RabbitMQ的方法详解,要实现 SpringBoot 连接多个 RabbitMQ,只能自定义重写一些东西,分别配置才可以,下面一起来走一下试试,需要的朋友可以参考下
    2023-10-10
  • JavaWeb中HttpSession中表单的重复提交示例

    JavaWeb中HttpSession中表单的重复提交示例

    这篇文章主要介绍了JavaWeb中HttpSession中表单的重复提交,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-03-03
  • Maven导入Junit4后在test中无法引用问题

    Maven导入Junit4后在test中无法引用问题

    在使用Maven进行项目管理时,可能会遇到导入依赖和打开项目结构的问题,本文通过实际经验,提供了一些解决方法和技巧,希望能帮助遇到相同问题的开发者,此外,还鼓励大家多多支持和分享个人经验,以便于共同进步
    2024-10-10

最新评论