前言

本文代码中有用到一些注解,主要是Lombok与junit用于简化代码。

主要是看到一堆代码会很乱,这样理解更清晰。如果没用过不用太过纠结。

对象的拷贝(克隆)是一个非常高频的操作,主要有以下三种方式:

  • 直接赋值
  • 拷贝:
    • 浅拷贝
    • 深拷贝

因为Java没有指针的概念,或者说是不需要我们去操心,这让我们省去了很多麻烦,但相应的,对于对象的引用、拷贝有时候就会有些懵逼,藏下一些很难发现的bug。

为了避免这些bug,理解这三种操作的作用与区别就是关键。

直接赋值

用等于号直接赋值是我们平时最常用的一种方式。

它的特点就是直接引用等号右边的对象

先来看下面的例子

先创建一个Person

@Data
@AllArgsConstructor
@ToString
public class Person{
private String name;
private int age;
private Person friend;
}

测试

@Test
public void test() {
Person friend =new Person("老王",30,null);
Person person1 = new Person("张三", 20, null);
Person person2 = person1;
System.out.println("person1: " + person1);
System.out.println("person2: " + person2 + "\n");
person1.setName("张四");
person1.setAge(25);
person1.setFriend(friend);
System.out.println("person1: " + person1);
System.out.println("person2: " + person2);
}

结果

person1: Person(name=张三, age=20, friend=null)
person2: Person(name=张三, age=20, friend=null) person1: Person(name=张四, age=25, friend=Person(name=老王, age=30, friend=null))
person2: Person(name=张四, age=25, friend=Person(name=老王, age=30, friend=null))

分析:

可以看到通过直接赋值进行拷贝,其实就只是单纯的对前对象进行引用。

如果这些对象都是基础对象当然没什么问题,但是如果对象进行操作,相当于两个对象同属一个实例

拷贝

直接赋值虽然方便,但是很多时候并不是我们想要的结果,很多时候我们需要的是两个看似一样但是完全独立的两个对象。

这种时候我们就需要用到一个方法clone()

clone()并不是一个可以直接使用的方法,需要先实现Cloneable接口,然后重写它才能使用。

protected native Object clone() throws CloneNotSupportedException;

clone()方法被native关键字修饰,native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是系统或者其他语言来实现。

浅拷贝

浅拷贝可以实现对象克隆,但是存在一些缺陷。

定义:

  • 如果原型对象的成员变量是值类型,将复制一份给克隆对象,也就是在堆中拥有独立的空间;
  • 如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,指向相同的内存地址。

举例

光看定义不太好一下子理解,上代码看例子。

我们先来修改一下Person类,实现Cloneable接口,重写clone()方法,其实很简单,只需要用super调用一下即可

@Data
@AllArgsConstructor
@ToString
public class Person implements Cloneable {
private String name;
private int age;
private Friend friend;
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
} ------- @Data
@AllArgsConstructor
public class Friend {
private String Name;
}

测试

@Test
public void test() {
Person person1 = new Person("张三", 20, "老王");
Person person2 = (Person) person1.clone(); System.out.println("person1: " + person1);
System.out.println("person2: " + person2 + "\n");
person1.setName("张四");
person1.setAge(25);
person1.setFriend("小王");
System.out.println("person1: " + person1);
System.out.println("person2: " + person2);
}

结果

person1: Person(name=张三, age=20, friend=Friend(Name=老王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王)) person1: Person(name=张四, age=25, friend=Friend(Name=小王))
person2: Person(name=张三, age=20, friend=Friend(Name=小王))

可以看到,name age基本对象属性并没改变,而friend引用对象熟悉变了。

原理

Java浅拷贝的原理其实是把原对象的各个属性的地址拷贝给新对象。

注意我说的是各个属性就算是基础对象属性其实也是拷贝的地址

你可能有点晕了,都是拷贝了地址,为什么修改了 person1 对象的 name age 属性值,person2 对象的 name age 属性值没有改变呢?

我们一步步来,拿name属性来说明:

  1. String、Integer 等包装类都是不可变的对象
  2. 当需要修改不可变对象的值时,需要在内存中生成一个新的对象来存放新的值
  3. 然后将原来的引用指向新的地址
  4. 我们修改了 person1 对象的 name 属性值,person1 对象的 name 字段指向了内存中新的 String 对象
  5. 我们并没有改变 person2 对象的 name 字段的指向,所以 person2 对象的 name 还是指向内存中原来的 String 地址

看图

这个图已经很清晰的展示了其中的过程,因为person1 对象改变friend时是改变的引用对象的属性,并不是新建立了一个对象进行替换,原本老王的消失了,变成了小王。所以person2也跟着改变了。

深拷贝

深拷贝就是我们拷贝的初衷了,无论是值类型还是引用类型都会完完全全的拷贝一份,在内存中生成一个新的对象。

拷贝对象和被拷贝对象没有任何关系,互不影响。

深拷贝相比于浅拷贝速度较慢并且花销较大。

简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

因为Java本身的特性,对于不可变的基本值类型,无论如何在内存中都是只有一份的。

所以对于不可变的基本值类型,深拷贝跟浅拷贝一样,不过并不影响什么。

实现:

想要实现深拷贝并不难,只需要在浅拷贝的基础上进行一点修改即可。

  • 给friend添加一个clone()方法。
  • Person类的clone()方法调用friendclone()方法,将friend也复制一份即可。
@Data
@ToString
public class Person implements Cloneable {
private String name;
private int age;
private Friend friend; public Person(String name, int age, String friend) {
this.name = name;
this.age = age;
this.friend = new Friend(friend);
} public void setFriend(String friend) {
this.friend.setName(friend);
} @Override
public Object clone() {
try {
Person person = (Person)super.clone();
person.friend = (Friend) friend.clone();
return person;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
} ------ @Data
@AllArgsConstructor
public class Friend implements Cloneable{
private String Name; @Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

测试

@Test
public void test() {
Person person1 = new Person("张三", 20, "老王");
Person person2 = (Person) person1.clone(); System.out.println("person1: " + person1);
System.out.println("person2: " + person2 + "\n");
person1.setName("张四");
person1.setAge(25);
person1.setFriend("小王");
System.out.println("person1: " + person1);
System.out.println("person2: " + person2);
}

结果

person1: Person(name=张三, age=20, friend=Friend(Name=老王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王)) person1: Person(name=张四, age=25, friend=Friend(Name=小王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王))

分析:

可以看到这次是真正的完全独立了起来。

需要注意的是,如果Friend类本身也存在引用类型,则需要在Friend类中的clone(),也去调用其引用类型的clone()方法,就如是Person类中那样,对!就是套娃!

所以对于存在多层依赖关系的对象,实现Cloneable接口重写clone()方法就显得有些笨拙了。

这里我们在介绍一种方法:利用序列化实现深拷贝

Serializable 实现深拷贝

修改PersonFriend,实现Serializable接口

@Data
@ToString
public class Person implements Serializable {
// ......同之前
public Object deepClone() throws Exception {
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); // 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis); return ois.readObject();
}
} --- @Data
@AllArgsConstructor
public class Friend implements Serializable {
private String Name;
}

测试

@Test
public void test() {
Person person1 = new Person("张三", 20, "老王");
Person person2 = null;
try {
person2 = (Person) person1.deepClone();
} catch (Exception e) {
e.printStackTrace();
} System.out.println("person1: " + person1);
System.out.println("person2: " + person2 + "\n");
person1.setName("张四");
person1.setAge(25);
person1.setFriend("小王");
System.out.println("person1: " + person1);
System.out.println("person2: " + person2);
}

结果

person1: Person(name=张三, age=20, friend=Friend(Name=老王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王)) person1: Person(name=张四, age=25, friend=Friend(Name=小王))
person2: Person(name=张三, age=20, friend=Friend(Name=老王))

只要将会被复制到的引用对象标记Serializable接口,通过序列化到方式即可实现深拷贝。

原理:

对象被序列化成流后,因为写在流里的是对象的一个拷贝,而原对象仍然存在于虚拟机里面

通过反序列化就可以获得一个完全相同的拷贝。

利用这个特性就实现了对象的深拷贝。

总结

  • 直接赋值是将新的对象指向原对象所指向的实例,所以一旦有所修改,两个对象会一起变。
  • 浅拷贝是把原对象属性的地址传给新对象,对于不可变的基础类型,实现了二者的分离,但对于引用对象,二者还是会一起改变。
  • 深拷贝是真正的完全拷贝,二者没有关系。实现深拷贝时如果存在多层依赖关系,可以采用序列化的方式来进行实现。

对于Serializable接口、Cloneable接口,其实都是相当于一个标记,点进去看源码,其实他们是一个空接口。

Java 轻松理解深拷贝与浅拷贝的更多相关文章

  1. java克隆之深拷贝与浅拷贝

    版权声明:本文出自汪磊的博客,转载请务必注明出处. Java深拷贝与浅拷贝实际项目中用的不多,但是对于理解Java中值传递,引用传递十分重要,同时个人认为对于理解内存模型也有帮助,况且面试中也是经常问 ...

  2. 浅谈Java中的深拷贝和浅拷贝(转载)

    浅谈Java中的深拷贝和浅拷贝(转载) 原文链接: http://blog.csdn.net/tounaobun/article/details/8491392 假如说你想复制一个简单变量.很简单: ...

  3. 浅谈Java中的深拷贝和浅拷贝

    转载: 浅谈Java中的深拷贝和浅拷贝 假如说你想复制一个简单变量.很简单: int apples = 5; int pears = apples; 不仅仅是int类型,其它七种原始数据类型(bool ...

  4. 内功心法 -- Java中的深拷贝和浅拷贝

    写在前面的话:读书破万卷,编码如有神--------------------------------------------------------------------这篇博客主要来谈谈" ...

  5. Java基础(十三)--深拷贝和浅拷贝

    在上篇文章:Java基础(十二)--clone()方法,我们简单介绍了clone()的使用 clone()对于基本数据类型的拷贝是完全没问题的,但是如果是引用数据类型呢? @Data @NoArgsC ...

  6. java中的深拷贝与浅拷贝

    Java中对象的创建 clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象.所谓的复制对象,首先要分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象.那 ...

  7. Java中的深拷贝和浅拷贝

    1.浅拷贝与深拷贝概念 (1)浅拷贝(浅克隆) 浅拷贝又叫浅复制,将对象中的所有字段复制到新的对象(副本)中.其中,值类型字段(java中8中原始类型)的值被复制到副本中后,在副本中的修改不会影响到源 ...

  8. Java中的深拷贝和浅拷贝(转载)

    深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在C++语言中,若不弄懂,则会在delete的时候出问题,但是我们在这幸好用的是Java.虽然java自动管理对象的回收,但对于深拷贝(深复 ...

  9. Java对象的深拷贝和浅拷贝、集合的交集并集

    http://blog.csdn.net/lian_1988/article/details/45970927 http://www.cnblogs.com/yxnchinahlj/archive/2 ...

随机推荐

  1. Vue学习笔记-django-cors-headers安装解决跨域问题

    一  使用环境: windows 7 64位操作系统 二  jango-cors-headers安装解决跨域问题(后端解决方案) 跨域,指的是浏览器不能执行其他网站的脚本.它是由浏览器的同源策略造成的 ...

  2. postman工具的用法

    1.postman发送json字符串的方法 1)设置header Content-Type   application/json 2)指定发送类型和json数据

  3. 第42天学习打卡(Class类 Class类的常用方法 内存分析 类的加载过程 类加载器 反射操作泛型 反射操作注解)

    Class类 对象照镜子后得到的信息:某个类的属性.方法和构造器.某个类到底实现了哪些接口.对于每个类而言,JRE都为其保留一个不变的Class类型的对象.一个Class对象包含了特定某个结构(cla ...

  4. Go benchmark 一清二楚

    前言 基准测试(benchmark)是 go testing 库提供的,用来度量程序性能,算法优劣的利器. 在日常生活中,我们使用速度 m/s(单位时间内物体移动的距离)大小来衡量一辆跑车的性能,同理 ...

  5. CISCN-2018-Quals-note-service2

    CISCN-2018-Quals-note-service2 总结 做完这道题,收获如下: 1)汇编语句jmp short s,最后编译出来的机器码为\xEB\x??,问号代表pc寄存器会往前或往后跳 ...

  6. [DP浅析]线性DP初步 - 2 - 单调队列优化

    目录 #0.0 前置知识 #1.0 简单介绍 #1.1 本质 & 适用范围 #1.2 适用方程 & 条件 #2.0 例题讲解 #2.1 P3572 [POI2014]PTA-Littl ...

  7. Celery:小试牛刀

    Celery是如何工作的? Celery 由于 其分布式体系结构,在某种程度上可能难以理解.下图是典型Django-Celery设置的高级示意图(FROM O'REILLY): 当请求到达时,您可以在 ...

  8. springboot系列五:springboot整合mybatisplus jsp

    一.用IDEA创建项目 1.添加pom.xml <?xml version="1.0" encoding="UTF-8"?> <project ...

  9. Mardown语法

    1.什么是Markdown Mardown是一种文本标记语言,使用它,能让我们更加专注于内容的输出,而不是排版样式. 我们平常使用的.txt文档书写的文字是没有样式的,使用Markdown语法就可以给 ...

  10. IPFS挖矿的成本有哪些?

    IPFS作为区块链新贵,近来风头一时无量.截止3月9日,Filecoin以257亿的流通市值超越门罗币,稳居区块链流通排行榜. 无论什么投资,其门槛一定在成本.今天就和大家细说投资市面上常见实体矿机的 ...