Java对象的复制三种方式(小结)

1、概述

在实际编程过程中,我们常常要遇到这种情况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能 会需要一个和A完全相同新对象B,并且此后对B任何改动都不会影响到A中的值,也就是说,A与B是两个独立的对象,但B的初始值是由A对象确定的。例如下面程序展示的情况:

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;
    stu1.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指向内存堆中同一个对象。如图:

那么,怎么能干干净净清清楚楚地复制一个对象呢。在 Java语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求有很多途径,

(1)将A对象的值分别通过set方法加入B对象中;
(2)通过重写java.lang.Object类中的方法clone();
(3)通过org.apache.commons中的工具类BeanUtils和PropertyUtils进行对象复制;
(4)通过序列化实现对象的复制。

2、将A对象的值分别通过set方法加入B对象中

对属性逐个赋值,本实例为了演示简单就设置了一个属性:

Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = new Student();
stu2.setNumber(stu1.getNumber());

我们发现,属性少对属性逐个赋值还挺方便,但是属性多时,就需要一直get、set了,非常麻烦。

3、重写java.lang.Object类中的方法clone()

先介绍一下两种不同的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。

在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制,下面将对两者进行详细介绍。

3.1 浅克隆

一般步骤:

被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口(不含任何方法)

覆盖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

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

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

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

3.2 深克隆

package abc; 

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

    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,地址:杭州市

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

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

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

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

4、工具类BeanUtils和PropertyUtils进行对象复制

Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = new Student();
BeanUtils.copyProperties(stu2,stu1);

这种写法无论多少种属性都只需要一行代码搞定,很方便吧!除BeanUtils外还有一个名为PropertyUtils的工具类,它也提供copyProperties()方法,作用与BeanUtils的同名方法十分相似,主要的区别在于BeanUtils提供类型转换功能,即发现两个JavaBean的同名属性为不同类型时,在支持的数据类型范围内进行转换,而PropertyUtils不支持这个功能,但是速度会更快一些。在实际开发中,BeanUtils使用更普遍一点,犯错的风险更低一点。

5、通过序列化实现对象的复制

序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可

以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

时间: 2019-08-24

深入java对象复制的分析

java本身提供了对象复制的能力,在java.lang.Object类中有clone方法,该方法是一个protected方法,在子类需要重写此方法并声明为public类型,而且还需实现Cloneable接口才能提供对象复制的能力,clone()是一个native方法,native方法的效率一般来说都是远高于java中的非native方法,对性能比较关心的话首先考虑这种方式,这种复制在网上有很多例子就不多写了:在这要用的另一种方式--通过java的反射机制复制对象,这种方式效率可能会比clone(

Java对象深复制与浅复制实例详解

 Java对象深复制与浅复制实例详解 我们在遇到一些业务场景的时候经常需要对对象进行复制,对于对象的复制一般有两种方式,深复制和浅复制 浅复制:对象的复制仅是对象本身,对象引用的其它对方并不会复制. 深复制:对象的复制包含对象引用的对象. Java所有对象的基类提供了clone方法,但是这个方法是protected native修饰,因此只暴露给之类去重写,外部是无法直接调用的. 我们现在来测试两种复制,首选是浅复制,浅复制要实现Cloneable接口. // 课程对象 class Class

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

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

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

克隆,想必大家都有耳闻,世界上第一只克隆羊多莉就是利用细胞核移植技术将哺乳动物的成年体细胞培育出新个体,甚为神奇.其实在Java中也存在克隆的概念,即实现对象的复制. 本文将尝试介绍一些关于Java中的克隆和一些深入的问题,希望可以帮助大家更好地了解克隆. 假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适

Java编程思想对象的容纳实例详解

Java提供了容纳对象(或者对象的句柄)的多种方式,接下来我们具体看看都有哪些方式. 有两方面的问题将数组与其他集合类型区分开来:效率和类型.对于Java来说,为保存和访问一系列对象(实际是对象的句柄)数组,最有效的方法莫过于数组.数组实际代表一个简单的线性序列,它使得元素的访问速度非常快,但我们却要为这种速度付出代价:创建一个数组对象时,它的大小是固定的,而且不可在那个数组对象的"存在时间"内发生改变.可创建特定大小的一个数组,然后假如用光了存储空间,就再创建一个新数组,将所有句柄从

Java编程Webservice指定超时时间代码详解

WebService是一种跨编程语言和跨操作系统平台的远程调用技术 所谓远程调用,就是一台计算机a上的一个程序可以调用到另外一台计算机b上的一个对象的方法,譬如,银联提供给商场的pos刷卡系统(采用交互提问的方式来加深大家对此技术的理解). 远程调用技术有什么用呢?商场的POS机转账调用的转账方法的代码是在银行服务器上,还是在商场的pos机上呢?什么情况下可能用到远程调用技术呢?例如,amazon,天气预报系统,淘宝网,校内网,百度等把自己的系统服务以webservice服务的形式暴露出来,让第

Java编程多线程之共享数据代码详解

本文主要总结线程共享数据的相关知识,主要包括两方面:一是某个线程内如何共享数据,保证各个线程的数据不交叉:一是多个线程间如何共享数据,保证数据的一致性. 线程范围内共享数据 自己实现的话,是定义一个Map,线程为键,数据为值,表中的每一项即是为每个线程准备的数据,这样在一个线程中数据是一致的. 例子 package com.iot.thread; import java.util.HashMap; import java.util.Map; import java.util.Random; /*

Java编程访问权限的控制代码详解

本文研究的主要是Java编程访问权限的控制的相关内容,具体介绍如下. 之前没去注意的修饰符,一般变量前面没添加,一个是不知道有什么用,一个是懒,后面遇到项目的时候就会发现私有和公有区别还是很大的. (1)首先是包名 使用一个类的时候,例如集合类,就需要引入这个包,然后再使用该包下面的类.如: package com.myown.iaiti; public class Print { static void print(String s){ System.out.println(s); } } 自

Java编程实现快速排序及优化代码详解

普通快速排序 找一个基准值base,然后一趟排序后让base左边的数都小于base,base右边的数都大于等于base.再分为两个子数组的排序.如此递归下去. public class QuickSort { public static <T extends Comparable<? super T>> void sort(T[] arr) { sort(arr, 0, arr.length - 1); } public static <T extends Comparabl

Java语言中的内存泄露代码详解

Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存.理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它的表现与C++不同. JAVA中的内存管理 要了解Java中的内存泄露,首先就得知道Java中的内存是如何管理的. 在Java程序中,我们通常使用new为对象分配内存,而这些内存空间都在堆(Heap)上. 下面看一个示例: public class Simple { public static vo

Java动态代理(设计模式)代码详解

基础:需要具备面向对象设计思想,多态的思想,反射的思想: Java动态代理机制的出现,使得Java开发人员不用手工编写代理类,只要简单地指定一组接口及委托类对象,便能动态地获得代理类.代理类会负责将所有的方法调用分派到委托对象上反射执行,在分派执行的过程中,开发人员还可以按需调整委托类对象及其功能,这是一套非常灵活有弹性的代理框架.通过阅读本文,读者将会对Java动态代理机制有更加深入的理解.本文首先从Java动态代理的运行机制和特点出发,对其代码进行了分析,推演了动态生成类的内部实现. 代理模

Java的静态类型检查示例代码详解

关于静态类型检查和动态类型检查的解释: 静态类型检查:基于程序的源代码来验证类型安全的过程: 动态类型检查:在程序运行期间验证类型安全的过程: Java使用静态类型检查在编译期间分析程序,确保没有类型错误.基本的思想是不要让类型错误在运行期间发生. 在各色各样的编程语言中,总共存在着两个类型检查机制:静态类型检查和动态类型检查. 静态类型检查是指通过对应用程序的源码进行分析,在编译期间就保证程序的类型安全. 动态类型检查是在程序的运行过程中,验证程序的类型安全.在Java中,编译期间使用静态类型

Java中EnumMap代替序数索引代码详解

本文研究的主要是Java中EnumMap代替序数索引的相关内容,具体介绍如下. 学习笔记<Effective Java 中文版 第2版> 经常会碰到使用Enum的ordinal方法来索引枚举类型. public class Herb { public enum Type { ANNUAL, PERENNIAL, BIENNIAL }; private final String name; private final Type type; Herb(String name, Type type)