java对象拷贝详解及实例

java对象拷贝详解及实例

Java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的:

@Test
public void testassign(){
 Person p1=new Person();
 p1.setAge(31);
 p1.setName("Peter");

 Person p2=p1;
 System.out.println(p1==p2);//true
}

如果创建一个对象的新的副本,也就是说他们的初始状态完全一样,但以后可以改变各自的状态,而互不影响,就需要用到java中对象的复制,如原生的clone()方法。

如何进行对象克隆

Object对象有个clone()方法,实现了对象中各个属性的复制,但它的可见范围是protected的,所以实体类使用克隆的前提是:

① 实现Cloneable接口,这是一个标记接口,自身没有方法。

② 覆盖clone()方法,可见性提升为public。

@Data
public class Person implements Cloneable {
  private String name;
  private Integer age;
  private Address address;
  @Override
  protected Object clone() throws CloneNotSupportedException {
    return super.clone();
  }
}

@Test
public void testShallowCopy() throws Exception{
 Person p1=new Person();
 p1.setAge(31);
 p1.setName("Peter");

 Person p2=(Person) p1.clone();
 System.out.println(p1==p2);//false
 p2.setName("Jacky");
 System.out.println("p1="+p1);//p1=Person [name=Peter, age=31]
 System.out.println("p2="+p2);//p2=Person [name=Jacky, age=31]
}

该测试用例只有两个基本类型的成员,测试达到目的了。

事情貌似没有这么简单,为Person增加一个Address类的成员:

@Data
public class Address {
  private String type;
  private String value;
}

再来测试,问题来了。

@Test
public void testShallowCopy() throws Exception{
 Address address=new Address();
 address.setType("Home");
 address.setValue("北京");

 Person p1=new Person();
 p1.setAge(31);
 p1.setName("Peter");
 p1.setAddress(address);

 Person p2=(Person) p1.clone();
 System.out.println(p1==p2);//false

 p2.getAddress().setType("Office");
 System.out.println("p1="+p1);
 System.out.println("p2="+p2);
}

查看输出:

false
p1=Person(name=Peter, age=31, address=Address(type=Office, value=北京))
p2=Person(name=Peter, age=31, address=Address(type=Office, value=北京))

遇到了点麻烦,只修改了p2的地址类型,两个地址类型都变成了Office。

浅拷贝和深拷贝

前面实例中是浅拷贝和深拷贝的典型用例。

浅拷贝:被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象。

深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象。

也就是说,一个默认的clone()方法实现机制,仍然是赋值。

如果一个被复制的属性都是基本类型,那么只需要实现当前类的cloneable机制就可以了,此为浅拷贝。

如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现cloneable接口并覆盖clone()方法。

@Data
public class Address implements Cloneable {
  private String type;
  private String value;

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

这样还不够,Person的clone()需要显式地clone其引用成员。

@Data
public class Person implements Cloneable {
  private String name;
  private Integer age;
  private Address address;
  @Override
  protected Object clone() throws CloneNotSupportedException {
    Object obj=super.clone();
    Address a=((Person)obj).getAddress();
    ((Person)obj).setAddress((Address) a.clone());
    return obj;
  }
}

重新跑前面的测试用例:

false
p1=Person(name=Peter, age=31, address=Address(type=Home, value=北京))
p2=Person(name=Peter, age=31, address=Address(type=Office, value=北京))

clone方式深拷贝小结

① 如果有一个非原生成员,如自定义对象的成员,那么就需要:

  1. 该成员实现Cloneable接口并覆盖clone()方法,不要忘记提升为public可见。
  2. 同时,修改被复制类的clone()方法,增加成员的克隆逻辑。

② 如果被复制对象不是直接继承Object,中间还有其它继承层次,每一层super类都需要实现Cloneable接口并覆盖clone()方法。

与对象成员不同,继承关系中的clone不需要被复制类的clone()做多余的工作。

一句话来说,如果实现完整的深拷贝,需要被复制对象的继承链、引用链上的每一个对象都实现克隆机制。

前面的实例还可以接受,如果有N个对象成员,有M层继承关系,就会很麻烦。

利用序列化实现深拷贝

clone机制不是强类型的限制,比如实现了Cloneable并没有强制继承链上的对象也实现;也没有强制要求覆盖clone()方法。因此编码过程中比较容易忽略其中一个环节,对于复杂的项目排查就是困难了。

要寻找可靠的,简单的方法,序列化就是一种途径。

1.被复制对象的继承链、引用链上的每一个对象都实现java.io.Serializable接口。这个比较简单,不需要实现任何方法,serialVersionID的要求不强制,对深拷贝来说没毛病。

2.实现自己的deepClone方法,将this写入流,再读出来。俗称:冷冻-解冻。

@Data
public class Person implements Serializable {
  private String name;
  private Integer age;
  private Address address;
  public Person deepClone() {
    Person p2=null;
    Person p1=this;
    PipedOutputStream out=new PipedOutputStream();
    PipedInputStream in=new PipedInputStream();
    try {
      in.connect(out);
    } catch (IOException e) {
      e.printStackTrace();
    }

    try(ObjectOutputStream bo=new ObjectOutputStream(out);
        ObjectInputStream bi=new ObjectInputStream(in);) {
      bo.writeObject(p1);
      p2=(Person) bi.readObject();

    } catch (Exception e) {
      e.printStackTrace();
    }
    return p2;
  }
}

原型工厂类

为了便于测试,也节省篇幅,封装一个工厂类。

公平起见,避免某些工具库使用缓存机制,使用原型方式工厂。

public class PersonFactory{
  public static Person newPrototypeInstance(){
    Address address = new Address();
    address.setType("Home");
    address.setValue("北京");

    Person p1 = new Person();
    p1.setAddress(address);
    p1.setAge(31);
    p1.setName("Peter");
    return p1;
  }
}

利用Dozer拷贝对象

Dozer是一个Bean处理类库。

maven依赖

<dependency>
 <groupId>net.sf.dozer</groupId>
 <artifactId>dozer</artifactId>
 <version>5.5.1</version>
</dependency>

测试用例:

@Data
public class Person {
  private String name;
  private Integer age;
  private Address address;

  @Test
  public void testDozer() {
  Person p1=PersonFactory.newPrototypeInstance();
    Mapper mapper = new DozerBeanMapper();
    Person p2 = mapper.map(p1, Person.class);
    p2.getAddress().setType("Office");
    System.out.println("p1=" + p1);
    System.out.println("p2=" + p2);
  }
}

@Data
public class Address {
  private String type;
  private String value;
}

输出:

p1=Person(name=Peter, age=31, address=Address(type=Home, value=北京))
p2=Person(name=Peter, age=31, address=Address(type=Office, value=北京))

注意:在万次测试中dozer有一个很严重的问题,如果DozerBeanMapper对象在for循环中创建,效率(dozer:7358)降低近10倍。由于DozerBeanMapper是线程安全的,所以不应该每次都创建新的实例。可以自带的单例工厂DozerBeanMapperSingletonWrapper来创建mapper,或集成到spring中。

还有更暴力的,创建一个People类:

@Data
public class People {
  private String name;
  private String age;//这里已经不是Integer了
  private Address address;

  @Test
  public void testDozer() {
  Person p1=PersonFactory.newPrototypeInstance();
    Mapper mapper = new DozerBeanMapper();
    People p2 = mapper.map(p1, People.class);
    p2.getAddress().setType("Office");
    System.out.println("p1=" + p1);
    System.out.println("p2=" + p2);
  }
}

只要属性名相同,干~

继续蹂躏:

@Data
public class People {
  private String name;
  private String age;
  private Map<String,String> address;//��

  @Test
  public void testDozer() {
  Person p1=PersonFactory.newPrototypeInstance();
    Mapper mapper = new DozerBeanMapper();
    People p2 = mapper.map(p1, People.class);
    p2.getAddress().put("type", "Office");
    System.out.println("p1=" + p1);
    System.out.println("p2=" + p2);
  }
}

利用Commons-BeanUtils复制对象

maven依赖

<dependency>
 <groupId>commons-beanutils</groupId>
 <artifactId>commons-beanutils</artifactId>
 <version>1.9.3</version>
</dependency>

测试用例:

@Data
public class Person {
  private String name;
  private String age;
  private Address address;

  @Test
  public void testCommonsBeanUtils(){
  Person p1=PersonFactory.newPrototypeInstance();
    try {
      Person p2=(Person) BeanUtils.cloneBean(p1);
      System.out.println("p1=" + p1);
      p2.getAddress().setType("Office");
      System.out.println("p2=" + p2);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

利用cglib复制对象

maven依赖:

<dependency>
 <groupId>cglib</groupId>
 <artifactId>cglib</artifactId>
 <version>3.2.4</version>
</dependency>

测试用例:

@Test
public void testCglib(){
 Person p1=PersonFactory.newPrototypeInstance();
 BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class, false);
 Person p2=new Person();
 beanCopier.copy(p1, p2,null);
 p2.getAddress().setType("Office");
 System.out.println("p1=" + p1);
 System.out.println("p2=" + p2);
}

结果大跌眼镜,cglib这么牛x,居然是浅拷贝。不过cglib提供了扩展能力:

@Test
public void testCglib(){
 Person p1=PersonFactory.newPrototypeInstance();
 BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class, true);
 Person p2=new Person();
 beanCopier.copy(p1, p2, new Converter(){
  @Override
  public Object convert(Object value, Class target, Object context) {
   if(target.isSynthetic()){
    BeanCopier.create(target, target, true).copy(value, value, this);
   }
   return value;
  }
 });
 p2.getAddress().setType("Office");
 System.out.println("p1=" + p1);
 System.out.println("p2=" + p2);
}

Orika复制对象

orika的作用不仅仅在于处理bean拷贝,更擅长各种类型之间的转换。

maven依赖:

<dependency>
 <groupId>ma.glasnost.orika</groupId>
 <artifactId>orika-core</artifactId>
 <version>1.5.0</version>
</dependency>
</dependencies>

测试用例:

@Test
public void testOrika() {
 MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

 mapperFactory.classMap(Person.class, Person.class)
 .byDefault()
 .register();
 ConverterFactory converterFactory = mapperFactory.getConverterFactory();
 MapperFacade mapper = mapperFactory.getMapperFacade();

 Person p1=PersonFactory.newPrototypeInstance();
 Person p2 = mapper.map(p1, Person.class);
 System.out.println("p1=" + p1);
 p2.getAddress().setType("Office");
 System.out.println("p2=" + p2);
}

Spring BeanUtils复制对象

给Spring个面子,貌似它不支持深拷贝。

Person p1=PersonFactory.newPrototypeInstance();
Person p2 = new Person();
Person p2 = (Person) BeanUtils.cloneBean(p1);
//BeanUtils.copyProperties(p2, p1);//这个更没戏

深拷贝性能对比

@Test
public void testBatchDozer(){
 Long start=System.currentTimeMillis();
 Mapper mapper = new DozerBeanMapper();
 for(int i=0;i<10000;i++){
  Person p1=PersonFactory.newPrototypeInstance();
  Person p2 = mapper.map(p1, Person.class);
 }
 System.out.println("dozer:"+(System.currentTimeMillis()-start));
 //dozer:721
}
@Test
public void testBatchBeanUtils(){
 Long start=System.currentTimeMillis();
 for(int i=0;i<10000;i++){
  Person p1=PersonFactory.newPrototypeInstance();
  try {
   Person p2=(Person) BeanUtils.cloneBean(p1);
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
 System.out.println("commons-beanutils:"+(System.currentTimeMillis()-start));
 //commons-beanutils:229
}
@Test
public void testBatchCglib(){
 Long start=System.currentTimeMillis();
 for(int i=0;i<10000;i++){
  Person p1=PersonFactory.newPrototypeInstance();
  BeanCopier beanCopier=BeanCopier.create(Person.class, Person.class, true);
  Person p2=new Person();
  beanCopier.copy(p1, p2, new Converter(){
   @Override
   public Object convert(Object value, Class target, Object context) {
    if(target.isSynthetic()){
     BeanCopier.create(target, target, true).copy(value, value, this);
    }
    return value;
   }
  });
 }
 System.out.println("cglib:"+(System.currentTimeMillis()-start));
 //cglib:133
}
@Test
public void testBatchSerial(){
 Long start=System.currentTimeMillis();
 for(int i=0;i<10000;i++){
  Person p1=PersonFactory.newPrototypeInstance();
  Person p2=p1.deepClone();
 }
 System.out.println("serializable:"+(System.currentTimeMillis()-start));
 //serializable:687
}
@Test
public void testBatchOrika() {
 MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

 mapperFactory.classMap(Person.class, Person.class)
 .field("name", "name")
 .byDefault()
 .register();
 ConverterFactory converterFactory = mapperFactory.getConverterFactory();
 MapperFacade mapper = mapperFactory.getMapperFacade();

 Long start=System.currentTimeMillis();
 for(int i=0;i<10000;i++){
  Person p1=PersonFactory.newPrototypeInstance();
  Person p2 = mapper.map(p1, Person.class);
 }
 System.out.println("orika:"+(System.currentTimeMillis()-start));
 //orika:83
}

@Test
public void testBatchClone(){
 Long start=System.currentTimeMillis();
 for(int i=0;i<10000;i++){
  Person p1=PersonFactory.newPrototypeInstance();
  try {
   Person p2=(Person) p1.clone();
  } catch (CloneNotSupportedException e) {
   e.printStackTrace();
  }
 }
 System.out.println("clone:"+(System.currentTimeMillis()-start));
 //clone:8
}

(10k)性能比较:

//dozer:721
//commons-beanutils:229
//cglib:133
//serializable:687
//orika:83
//clone:8

深拷贝总结

原生的clone效率无疑是最高的,用脚趾头都能想到。

偶尔用一次,用哪个都问题都不大。

一般性能要求稍高的应用场景,cglib和orika完全可以接受。

另外一个考虑的因素,如果项目已经引入了某个依赖,就用那个依赖来做吧,没必要再引入一个第三方依赖。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

(0)

相关推荐

  • java使用nio2拷贝文件的示例

    这个程序只是为了更方便的进行拷贝文件(夹)而创造.1.可以不用新建文件夹,就像windows的复制粘贴一样简单.2.有简单的出错重连机制3.不需要重复拷贝,差异化复制文件.4.拷贝文件夹的时候可以不用复制全路径,只关注需要拷贝的文件夹.5.程序做了简单的必要检查,效率也不算低.6.使用的是7的nio2的新API. 复制代码 代码如下: import java.io.IOException;import java.nio.file.FileVisitResult;import java.nio.f

  • 深入理解java中的拷贝机制

    前言 众所周知在Java中,拷贝分为深拷贝和浅拷贝两种.java在公共超类Object中实现了一种叫做clone的方法,这种方法clone出来的新对象为浅拷贝,而通过自己定义的clone方法为深拷贝. (一)Object中clone方法 如果我们new出一个新对象,用一个声明去引用它,之后又用另一个声明去引用前一个声明,那么最后的结果是:这两个声明的变量将指向同一个对象,一处被改全部被改.如果我们想创建一个对象的copy,这个copy和对象的各种属性完全相同,而且修改这个copy和原对象毫无关系

  • java 深拷贝与浅拷贝机制详解

     java 深拷贝与浅拷贝机制详解 概要: 在Java中,拷贝分为深拷贝和浅拷贝两种.java在公共超类Object中实现了一种叫做clone的方法,这种方法clone出来的新对象为浅拷贝,而通过自己定义的clone方法为深拷贝. (一)Object中clone方法 如果我们new出一个新对象,用一个声明去引用它,之后又用另一个声明去引用前一个声明,那么最后的结果是:这两个声明的变量将指向同一个对象,一处被改全部被改.如果我们想创建一个对象的copy,这个copy和对象的各种属性完全相同,而且修

  • Java中的深拷贝和浅拷贝介绍

    一.引言   对象拷贝(Object Copy)就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去.在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用对象的部分或全部 数据.Java中有三种类型的对象拷贝:浅拷贝(Shallow Copy).深拷贝(Deep Copy).延迟拷贝(Lazy Copy). 二.浅拷贝 1.什么是浅拷贝   浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝.如果属性是基本类型,拷贝的就是基本类型的值:如果属性是内存地

  • java 实现文件夹的拷贝实例代码

    java 实现文件夹的拷贝实例代码         这里就直接上代码,废话不多说,很简单很实用. 实例代码: import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; public class CopyFile { public static void copy(String sourceFile , Str

  • 探讨java深拷贝

    本文将讨论以下4个问题 1. java Cloneable接口实现深拷贝     2. java 序列化实现深拷贝     3. 号称最快的深拷贝二方库cloning源码分析     4. 几种拷贝方式速度的比较 深拷贝的概念本文就不说了.在C++中实现深拷贝一般情况下重载赋值操作符 "=" 来实现同一个类的对象间的深拷贝,所以很自然的在java中我们也同样可以定义一个copy函数,在函数内部为对象的每一个属性作赋值操作.这种方式简单自然,但存在一个致命性的问题:如果有一天在类中新增加

  • java文件复制代码片断(java实现文件拷贝)

    一.要完成这个程序需要了解的知识点: 1.编写简单的Java程序,比如hello world ---废话了....哈哈 2.了解java的文件操作 3.了解java的buffer操作 4.对文件操作的一些异常处理点:1.源文件不能读取到的情况. 2.目的文件创建失败的情况 3.文件锁问题 4.字符乱码问题...可能不全啊 这些是需要用到的包 import java.io.BufferedInputStream; import java.io.BufferedOutputStream; impor

  • java对象拷贝详解及实例

    java对象拷贝详解及实例 Java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的: @Test public void testassign(){ Person p1=new Person(); p1.setAge(31); p1.setName("Peter"); Person p2=p1; System.out.println(p1==p2);//true } 如果创建一个对象的新的副本,也就是说他们的初始状态完全一样,但以后可以改变各自的状态,

  • java 泛型的详解及实例

    java 泛型的详解及实例 Java在1.5版本中增加了泛型,在没有泛型之前,从集合中读取每一个对象都需要进行强转,如果一不小心插入了类型错误的对象,在运行时就会报错,给日常开发带来了很多不必要的麻烦,比如以下代码: public class TestGeneric { public static void main(String[] args) { List list = new ArrayList(); list.add(" name:"); list.add(" zer

  • Java List 用法详解及实例分析

    Java List 用法详解及实例分析 Java中可变数组的原理就是不断的创建新的数组,将原数组加到新的数组中,下文对Java List用法做了详解. List:元素是有序的(怎么存的就怎么取出来,顺序不会乱),元素可以重复(角标1上有个3,角标2上也可以有个3)因为该集合体系有索引 ArrayList:底层的数据结构使用的是数组结构(数组长度是可变的百分之五十延长)(特点是查询很快,但增删较慢)线程不同步 LinkedList:底层的数据结构是链表结构(特点是查询较慢,增删较快) Vector

  • Java 反射机制详解及实例

    Java 反射机制详解及实例 反射,当时经常听他们说,自己也看过一些资料,也可能在设计模式中使用过,但是感觉对它没有一个较深入的了解,这次重新学习了一下,感觉还行吧!            一,先看一下反射的概念: 主要是指程序可以访问,检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义. 反射是Java中一种强大的工具,能够使我们很方便的创建灵活的代码,这些代码可以再运行时装配,无需在组件之间进行源代码链接.但是反射使用不当会成本很高

  • java 内部类的详解及实例

     java 内部类的详解及实例 内部类的存在意义: 每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响.内部类使得多重继承的解决方案变得完整.接口解决了部分问题,二内部类有效地实现了"多重继承".(这是<Java编程思想>里面的解释) 代码: public class OuterAndInnerClass { public static void main(String[] args) { //创建内部类对象的

  • Java反射技术详解及实例解析

    前言 相信很多人都知道反射可以说是Java中最强大的技术了,它可以做的事情太多太多,很多优秀的开源框架都是通过反射完成的,比如最初的很多注解框架,后来因为java反射影响性能,所以被运行时注解APT替代了,java反射有个开源框架jOOR相信很多人都用过,不过我们还是要学习反射的基础语法,这样才能自己写出优秀的框架,当然这里所讲的反射技术,是学习Android插件化技术.Hook技术等必不可少的! 一.基本反射技术   1.1 根据一个字符串得到一个类 getClass方法 String nam

  • Java中双向链表详解及实例

    Java中双向链表详解及实例 写在前面: 双向链表是一种对称结构,它克服了单链表上指针单向性的缺点,其中每一个节点即可向前引用,也可向后引用,这样可以更方便的插入.删除数据元素. 由于双向链表需要同时维护两个方向的指针,因此添加节点.删除节点时指针维护成本更大:但双向链表具有两个方向的指针,因此可以向两个方向搜索节点,因此双向链表在搜索节点.删除指定索引处节点时具有较好的性能. Java语言实现双向链表: package com.ietree.basic.datastructure.dublin

  • Java中自定义异常详解及实例代码

    Java中自定义异常详解及实例代码 下面做了归纳总结,欢迎批评指正 自定义异常 class ChushulingException extends Exception { public ChushulingException(String msg) { super(msg); } } class ChushufuException extends Exception { public ChushufuException(String msg) { super(msg); } } 自定义异常 En

  • java list 比较详解及实例

    java list 比较详解及实例 java里比较两个list的值是否一致,不考虑顺序,有多种方法,比如排序后直接用equals比较,相互之间执行两次containsAll等,这些办法都需要我们给list的元素类实现equals和hashcode方法.但是有一种特殊情况,如果我们并不方便去实习类的equals方法,例如是一个古老的第三方jar包,改代码会带来很多未知问题,这时候该怎么办呢. 其实很简单,万能的apache-commons早就想到了这一点,所以在commons-collection

  • Java 线程优先级详解及实例

    Java 线程优先级详解及实例 操作系统基本采用时分的调度运行线程,操作系统会分出一个个时间片,线程会被分配到若干个时间片,当线程的时间片用完了就会发生线程调度,并且等待着下次调度,线程被分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程能够分配多少处理器资源的线程属性. 在Java多线程中,通过一个整形变量priority来控制优先级,优先级的范围从1-10.默认是5,优先级越高越好. public class Priority { public static vo

随机推荐