浅拷贝&深度拷贝
-
对象的克隆
如果想修改一个对象但又不想影响其他引用,可以使用clone()方法。虽然Object定义了克隆方法,但是并不是所有类都具备克隆能力。要想能够执行clone()方法,需要实现Cloneable接口。如果不实现该接口则会抛出CloneNotSupportedException异常。 -
浅拷贝
在拷贝过程中,只拷贝基本类型和引用,引用中对象的值则不会被拷贝。 对象的克隆一般都是浅拷贝,比如ArrayList的clone就是浅拷贝。
/**
* Returns a shallow copy of this <tt>ArrayList</tt> instance. (The
* elements themselves are not copied.)
*
* @return a clone of this <tt>ArrayList</tt> instance
*/
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
在ArrayList的clone实现的是shallow copy(浅拷贝)。手写一段简单代码测试一下:
public static void main(String[] args) throws CloneNotSupportedException {
ArrayList<User> list = new ArrayList<>();
User user1 = new User();
user1.setAge("18");
user1.setName("white");
list.add(user1);
ArrayList<User> list1 = (ArrayList<User>) list.clone();
user1.setName("green");
list1.forEach(i-> System.out.println(i.getName()));
}
以上代码输出结果为:green
虽然list1是list的克隆,但是浅拷贝只复制了引用,因此修改list的user仍然会影响到list1。如果想彻底是两个list不产生相互影响就需要进行深度拷贝。
-
深度拷贝
不仅拷贝基本类型,引用中的对象也拷贝则成为深度拷贝。java的克隆方法不提供深度拷贝。需要自己根据实际情况自己来实现。
拷贝的实现方式
除了上面提到的实现Cloneable接口外,还有第三方提供的一些拷贝方式可以进行参考。
1. BeanUtils
spring提供了BeanUtils来实现对象的克隆,基本原理大体上就是使用反射机制来实现的。BeanUtils实现的并不是深度拷贝,不过BeanUtils可以拷贝两个不同的对象,这一点还是很不错的。
2. 序列化拷贝
如果想实现深度拷贝,最简单的方式我觉得是通过序列化的方式来实现。序列化有多种方式,我在这里使用JSON来进行序列化。大体思路是将对象转换成JSON,然后再将json转换成指定对象。
public static void main(String[] args) throws CloneNotSupportedException {
ArrayList<User> list = new ArrayList<>();
User user1 = new User();
user1.setAge("18");
user1.setName("white");
list.add(user1);
String json = JSON.toJSONString(list);
List<User> list2 = JSON.parseArray(json,User.class);
user1.setName("green");
list2.forEach(i-> System.out.println(i.getName()));
}
结果为:white 修改list的user不会影响到list2。
通过序列化进行拷贝的方式效率很低,在使用时需要三思而后行。如何可以通过浅拷贝实现就尽量不要深度拷贝,如果可以手动拷贝的就尽量不要用序列化进行拷贝。以免造成不必要的性能浪费。
进一步的思考
String是Java中的对象,为什么进行克隆以后String并不会被影响呢?浅拷贝只会拷贝String引用不是吗?在《java编程思想》里将String定义为“只读类”或者叫做“恒常对象”。简单的说就是这种对象不提供修改的方法,因此就无需担心其他的引用对其的影响了。同时它的缺陷也非常明显,如果要修改内容就只能重新创建新的对象,这加大了内存的开销同时会频繁的引发垃圾回收,代价是比较高昂的。
因此提出了可以被修改的伴随类,伴随类在使用过程中可以任意修改,修改完成后再转换成只读类,StringBuffer就是一个伴随类,toString()就是转换成只读类的过程。