目录
2.1 浅拷贝实现(实现Cloneable的接口,重写clone方法)
2.2.2 Apache库SerializationUtils
2.2.3 JSON Serialization With Gson
2.2.4 JSON Serialization With Jackson
讲述对象的深浅拷贝原理和简单代码实现
(篇幅原因省略getter setter方法)
1、理论或者业务背景
简单介绍几种相关名词(可只看前3个):
浅拷贝:拷贝出一个新对象,新对象与原对象地址不同,但是原对象与新对象的属性指向地址都是同一个;
深拷贝:原对象与新对象的属性指向堆空间中内存地址不同的对象;
序列化:Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型[1][runoob]。简单来说就是把对象保存到文件里,要用的时候再取出来。
值传递:顾名思义。传递数值。(浅拷贝,对象属性没有序列化的话,是不会被拷贝到的,与这个有关,下同)
引用传递:顾名思义。传递引用或者地址。
垃圾回收:没有引用的对象会被清除掉(这个看起来不相关,实际上这个可以辅助理解值传递和引用传递)
值传递和引用传递作为讨论旁支在“3、原理”中会再讨论
2、代码
2.0 对象定义
/**
* @Description 水果信息
* @Author: Zeng Yanru
* @Date: 2021/7/4 10:44
*/
public class FruitPO {
private String name;
private CategoryPO category;
private String color;
}
/**
* @Description 水果分类信息
* @Author: Zeng Yanru
* @Date: 2021/7/4 10:44
*/
public class CategoryPO {
private String name;
private Integer categoryId;
}
2.1 浅拷贝实现(实现Cloneable的接口,重写clone方法)
对象:
/**
* @Description 水果信息
* @Author: Zeng Yanru
* @Date: 2021/7/4 10:44
*/
public class FruitPO implements Cloneable{
private String name;
private CategoryPO category;
private String color;
@Override
public FruitPO clone() {
FruitPO fruit = null;
try {
fruit = (FruitPO) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return fruit;
}
}
方法:
public class FruitService {
public static void main(String[] args) {
// 苹果
FruitPO apple = new FruitPO();
CategoryPO appleCtg = new CategoryPO();
appleCtg.setName("apple_cate");
apple.setName("apple");
apple.setCategory(appleCtg);
// 梨子拷贝
FruitPO pear = apple.clone();
// 直接原位修改category
pear.setName("pear");
pear.getCategory().setName("pear_cate");
System.out.println("苹果名字:" + apple.getName());
System.out.println("苹果分类名字:" + apple.getCategory().getName());
System.out.println("------");
System.out.println("梨子名字:" + pear.getName());
System.out.println("梨子分类名字:" + pear.getCategory().getName());
}
}
结果:
苹果名字:apple
苹果分类名字:pear_cate
------
梨子名字:pear
梨子分类名字:pear_cate
结论:浅拷贝拷贝的只有对象自己,其引用还是没有改变。需要将引用也拷贝,则需要对引用的对象也实现Cloneable的接口,重写clone方法。[2][cnblog]
2.2 深拷贝实现
2.2.1 序列化
方法:就是存文件,读文件
public class FruitService {
public static void main(String[] args) {
// 苹果
FruitPO apple = new FruitPO();
CategoryPO appleCtg = new CategoryPO();
appleCtg.setName("apple_cate");
apple.setName("apple");
apple.setCategory(appleCtg);
// 梨子拷贝
FruitPO pear = deepClone(apple);
// 直接原位修改category
pear.setName("pear");
pear.getCategory().setName("pear_cate");
System.out.println("苹果名字:" + apple.getName());
System.out.println("苹果分类名字:" + apple.getCategory().getName());
System.out.println("------");
System.out.println("梨子名字:" + pear.getName());
System.out.println("梨子分类名字:" + pear.getCategory().getName());
}
public static <T> T deepClone(T object) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (T) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
结果:
苹果名字:apple
苹果分类名字:apple_cate
------
梨子名字:pear
梨子分类名字:pear_cate
结论:
深拷贝(序列化这种方法)有效,引用也拷贝一份
2.2.2 Apache库SerializationUtils
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
只需要换成:
import org.apache.commons.lang3.SerializationUtils;
......
// 梨子拷贝
FruitPO pear = SerializationUtils.clone(apple);
结果:
苹果名字:apple
苹果分类名字:apple_cate
------
梨子名字:pear
梨子分类名字:pear_cate
结论:
同样是深拷贝
2.2.3 JSON Serialization With Gson
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
同理可换成:
// 梨子拷贝
Gson gson = new Gson();
FruitPO pear = gson.fromJson(gson.toJson(apple), FruitPO.class);
结果
苹果名字:apple
苹果分类名字:apple_cate
------
梨子名字:pear
梨子分类名字:pear_cate
结论:
深拷贝
2.2.4 JSON Serialization With Jackson
故技重施与2.2.3类似,此略。
3、原理和讨论
引用传递和值传递,在java中经常会搞混,(java只有值传递)。因为会出现类似于这样的情况:
//我摘抄这位作者的代码:
//作者:Intopass
//链接:https://www.zhihu.com/question/31203609/answer/50992895
//来源:知乎
//著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
第一个例子:基本类型
void foo(int value) {
value = 100;
}
foo(num); // num 没有被改变
第二个例子:没有提供改变自身方法的引用类型
void foo(String text) {
text = "windows";
}
foo(str); // str 也没有被改变
第三个例子:提供了改变自身方法的引用类型
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
builder.append("4");
}
foo(sb); // sb 被改变了,变成了"iphone4"。
第四个例子:提供了改变自身方法的引用类型,但是不使用,而是使用赋值运算符。
StringBuilder sb = new StringBuilder("iphone");
void foo(StringBuilder builder) {
builder = new StringBuilder("ipad");
}
foo(sb); // sb 没有被改变,还是 "iphone"。
实际上java对于命名、内存地址、是否有改变自身的方法(getter,setter,append等)都有考量。考虑到这三者,便可了解写的对象,其值是否有改变;
第一个,第二个例子:(没有改变自身的方法+作用域不同)
第三个例子:(改变自身的方法+作用域不同)
第四个例子:(new对象+作用域不同)
稍微整理一下,也挺好理解的
4、参考资料
[runoob][https://www.runoob.com/java/java-serialization.html]
[cnblog][https://www.cnblogs.com/fnlingnzb-learner/p/10649509.html]