Java - Deep Copy
Created by : Mr Dk.
2019 / 10 / 07 20:12
Nanjing, Jiangsu, China
About
最近在用 Java 实现一个 Fuzzer 时,其中的一些算法需要对对象进行拷贝。之前写 JavaScript 的时候,都是直接 JSON.parse() + JSON.stringfy() 完事了,对于 Java 应该怎么处理不是太了解。于是搜索了一波 🙄 名堂还真不少,但至少可以明确的一点是:JSON 不是 Java 的原生支持。所以方法显然不是转成 JSON 字符串再转回来。
Clone Function
Java 中的所有对象全部继承自 java.lang.Object。在这个 root superclass 中,定义了一个 clone() 函数,因此任何派生类都可以 override 这一个 clone() 函数。该函数的默认行为是 - 返回一个对象的 浅拷贝:
- 复制该对象中的每一个 field
- 对于 原始数据类型 (
int,float) 或 不可改变的类型 (String) 来说,复制的是值 - 对于对象来说,复制的是引用 (我宁愿把它理解为,复制的是指针) - 只是复制了指针,指针变成了两个,但指向的区域还是同一个
浅拷贝的问题显而易见,如果通过复制后的对象中的对象引用 修改 (只读应该不会有问题) 了对象数据,那么被复制对象中的引用的对象数据也会发生变化。因为他们引用同一个对象。例子:
Vector original = new Vector();
StringBuffer text = new StringBuffer("A");
original.addElement(text);
Vector clone = (Vector) original.clone();
实例化一个 Vector 对象,并将一个 StringBuffer 对象作为元素加入到该对象中 (引用传递)。然后浅拷贝一个 Vector 对象,显然,通过两个对象都可以访问到 StringBuffer 对象,克隆看起来是成功的。
clone.addElement(new Integer(5));
此时,访问这两个对象, clone 可以访问到 Integer,而 original 不行。因为这两个 Vector 现在确实是两个独立的对象了。clone 对象持有对 Integer 对象的引用,而 original 对象没有。
text.append("EMMM");
此时,通过两个 Vector 对象访问 text,发现两个对象中的 text 对象都变了。因为这两个 Vector 对象持有对同一个 StringBuffer 对象的引用。
也就是说,浅拷贝只是复制了 指针,而指针指向的对象没有被复制。因此,通过
origin对象修改了StringBuffer对象后,通过clone对象访问StringBuffer对象时,能够看到修改。
so. 如果业务逻辑需要的是把 StringBuffer 对象也复制两份,即需要进行所谓的 deep copy,how to do it?
Issues
或许可以自己实现一个 deepCopy() 函数,以完成复杂的深拷贝功能?
- 必须有这个类的源代码才行 - 如果想复制一个第三方的类,或许可能被声明为
final- Well...GG 😥 - 必须能够访问这个类的基类的所有 field - 如果基类的 field 被声明为
private- Well...GG again 😥 - 必须能够拷贝该类引用的所有类型的对象才行 - 但有些类型只有在运行时才能被明确
- 很容易出错,难于维护 - 一旦这个类的结构被改动了,都要检查一遍这个函数
此外,重写 clone() 函数在 StackOverFlow 上也不被推荐,因为深度复制应当是一个递归的过程。至少在实现 clone() 函数时,该对象引用的所有类型的对象也应当都覆盖 clone(),不然总会有被该类引用的对象是浅拷贝。
emm... 🤔 有道理啊...
Solution
所以常用的解决方案是使用 Java Object Serialization (JOS),即序列化和反序列化。从字节层面,把整个对象复制一份。更重要的是,JOS 负责其中的所有细节:
- 父类中的 field
- 跟随 object graphs 并能够递归地复制引用的所有对象
意思是只管序列化 / 反序列化就行,不用管嵌套对象的问题。
Object orig;
Object obj = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(orig);
out.flush();
out.close();
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
obj = in.readObject();
但也存在问题:
被拷贝的对象 (包括嵌套在内的) 必须实现
java.io.Serializable- 即,必须是可序列化的那第三方的对象万一无法被序列化咋办呢?它没有实现
java.io.Serializable接口,我还要修改它的源代码不成??JOS 比较慢
Byte array stream 的实现是 线程安全 的,因此会有一些额外的开销
可能的优化方式:
ByteArrayOutputStream默认会初始化 32 字节的数组,然后动态增长 - 那么可以用更大的值初始化ByteArrayOutputStream的所有函数都是synchronized的 - 由于我们确保是在单线程中执行,同步关键字可以去掉toByteArray()会返回 stream 的字节数组的一份拷贝 - 浪费空间和时间
Reference
Java Techniques - Faster Deep Copies of Java Objects
Summary
还是需要多读读 JDK 源码。