目录
Java是值传递还是引用传递
什么是深拷贝和浅拷贝
浅拷贝案例解析
BeanUtils的浅拷贝
实现深拷贝
深拷贝案例解析
-
Java是值传递还是引用传递
- 编程语言中需要进行方法间的参数传递,这个传递的策略叫做求值策略
- 在程序设计中,求值策略有很多种,比较常见的就是值传递和引用传递
- 按值调用(call by value)表示方法接收的是调用者提供的值
- 而按引用调用(call byreference)表示方法接收的是调用者提供的变量地址
- 一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值
- 值传递和引用传递最大的区别是传递的过程中有没有复制出一个副本来,如果是传递副本,那就是值传递,否则就是引用传递
- Java只有值传递
- Java对象的传递,是通过复制的方式把引用关系传递了,因为有复制的过程,所以是值传递,只不过对于Java对象的传递,传递的内容是对象的引用
-
什么是深拷贝和浅拷贝
- 在计算机内存中,每个对象都有一个地址,这个地址指向对象在内存中存储的位置
- 当使用变量引用一个对象时,实际上是将该对象的地址赋值给变量
- 因此如果将一个对象复制到另一个变量中,实际上是将对象的地址复制到了这个变量中
- 浅拷贝——拷贝的太“浅”了,只拷贝对象引用,即对象的地址
- 浅拷贝是指将一个对象复制到另一个变量中,但是只复制对象的地址,而不是对象本身
- 也就是说,原始对象和复制对象实际上是共享同一个内存地址的
- 因此如果修改了复制对象中的属性或元素,原始对象中对应的属性或元素也会被修改
- 在Java中常用的各种BeanUtils基本也都是浅拷贝的
- 深拷贝——拷贝的很“深”,复制对象的值到新开辟的空间,副本和原来的值没有任何耦合,真正意义的‘拷贝’
- 深拷贝是指将一个对象及其所有子对象都复制到另一个变量中,也就是说,它会创建一个全新的对象,并将原始对象中的所有属性或元素都复制到新的对象中
- 因此如果修改复制对象中的属性或元素,原始对象中对应的属性或元素不会受到影响
-
浅拷贝案例解析
- 定义:
- 基本数据类型:short,int,long,float,double,char,byte,boolean
- 特殊类型:String(这个类型比较重要)
- String 存在于堆内存、常量池;这种比较特殊,本身没有实现 Cloneable,传递是引用地址
- 由本身的final性,每次赋值都是一个新的引用地址,原对象的引用和副本的引用互不影响
- 因此String就和基本数据类型一样,表现出了"深拷贝"特性
- 代码以 学生背书包 为例,学生类Student,书包类Bag
-
浅拷贝
- 使用Clone()方法必须实现接口Cloneable,默认实现的就是浅拷贝(引用拷贝)
- 看测试结果,一目了然
- 运行如下:
-
BeanUtils的浅拷贝
- 举个实际例子,来看下为啥前面说BeanUtils.copyProperties的过程是浅拷贝
- 先来定义两个类:
- 然后写一段测试代码:
- 以上代码输出结果为:
- 即,BeanUtils.copyProperties拷贝出来的newUser是一个新的对象,但是其中的address对象和原来的user中的address对象是同一个对象
- 如果修改newUser中的Address对象的值的话,是会同时把user对象中的Address的值也修改了的
- 可以尝试着修改下newUser中的address对象:
- 输出结果:
-
实现深拷贝
- 如何实现深拷贝呢,主要有以下几个方法:
- 实现Cloneable接口,重写clone()
- 在Object类中定义了一个clone方法,这个方法其实在不重写的情况下,其实也是浅拷贝的
- 如果想要实现深拷贝,就需要重写clone方法,而想要重写clone方法,就必须实现Cloneable,否则会报CloneNotSupportedException异常
- 将上述代码修改下,重写clone方法:
- 之后再执行一下上面的测试代码,就可以发现,这时候newUser中的address对象就是一个新的对象了
- 这种方式就能实现深拷贝,但是问题是如果我们在User中有很多个对象,那么clone方法就写的很长,而且如果后面有修改,在User中新增属性,这个地方也要改
- 那么有没有什么办法可以不需要修改,一劳永逸呢
- 序列化实现深拷贝
- 可以借助序列化来实现深拷贝
- 先把对象序列化成流,再从流中反序列化成对象,这样就一定是新的对象了
- 序列化的方式有很多,比如可以使用各种JSON工具,把对象序列化成JSON字符串,然后再从字符串中反序列化成对象
- 如使用fastjson实现:
- 也可实现深拷贝
- 除此之外,还可以使用Apache Commons Lang中提供的SerializationUtils工具实现
- 需要修改下上面的User和Address类,使他们实现Serializable接口,否则是无法进行序列化的
- 然后在需要拷贝的时候:
- 同样也可以实现深拷贝
- 实现Cloneable接口,重写clone()
-
深拷贝案例解析
- 实现Cloneable
- 还是之前的Student和Bag类,可以看到在浅拷贝Student的时候,Bag是复合数据类型,浅拷贝的是这个Bag的引用,指向同一个地址
- 那么深拷贝的话,同样需要将Bag复制一份到新的地址,实现Cloneable的类本身是进行深拷贝的,默认浅拷贝的方法是拷贝的该对象里面的复合数据类型,那么如果要将Student进行深拷贝,那么Bag也需要像Student一样实现Cloneable接口,并重写Clone()
- 修改Bag.java
- Bag也实现了Cloneable,那么在调用Student.clone()的时候,首先要使用Bag.clone()将bag的对象进行一次拷贝
- 测试代码:
- 测试结果如下:
- 至此深拷贝和浅拷贝基本看完了
- 但是如果Bag类型里面也含有复合数据类型呢?
- 直接用上面的深拷贝代码,肯定是不行,不信看代码:
- 假如学生背书包,书包里面有笔(钢笔、圆珠笔、铅笔...)
- 测试代码:
- 测试结果:
- 由此看来直接这么做是无法完成深拷贝的,因为上述代码并不是完全的深拷贝,问题就出在书包Bag中的成员Pen
- 既然发现了问题,那么要实现完全的深拷贝就简单了
- 1.将Pen.java也实现cloneable接口,并且重写clone()
- 2.在Bag.java中的clone()方法使用pen.clone()深拷贝一份到bag中的pen成员中
- 问题又来了,如果Pen中又有一个复杂数据类型呢?
- 岂不是又要将Pen中的复杂数据类型也要做同样的操作?
- 是的,object的clone()方法,在深拷贝的时候,不适用于有对象多层嵌套的情况
- 序列化
- 上面已经暴露了简单使用clone()进行深拷贝的弊端,这里介绍另一种深拷贝的方式——序列化深拷贝
- 所有bean必须实现 Serializable
- 可以直接在Student中实现序列化【方法一】
- 或者,直接将深拷贝方法包装成一个工具类【方法二:推荐该方法】:
- 测试代码:
- 测试结果如下: