深复制与浅复制
Object中的clone()方法在对某个对象实施克隆时对其是一无所知的,它仅仅是简单地执行域对域的copy,这就是Shallow Clone。这样的话如果有一个类为ClassA,它里面有一个域a不是基本类型的变量,而是一个reference变量,经过Clone之后克隆类只会产生一个新的A类型的引用,它和原始引用都指向同一个a对象,这样克隆类就和原始类共享了一部分信息,这就是浅复制。(八种基本类型除外,他们可以自动实现深复制)
不可变类与clone
String,Integer等对象在浅克隆时会被克隆
public class Main implements Cloneable{
String value;
Integer id;
Main son;
public static void main(String[] args) throws CloneNotSupportedException {
Main a=new Main();
a.value="a";
a.id=1;
Main ason=new Main();
ason.value="ason";
a.son=ason;
Main aclone=(Main)a.clone();
System.out.println(aclone.value);//a
System.out.println(aclone.son.value);//ason
System.out.println(aclone.id);//1
a.value="newa";
a.son.value="newason";
a.id=2;
System.out.println(aclone.value);//a
System.out.println(aclone.son.value);//newason
System.out.println(aclone.id);//1
}
}
String 和 Integer 类成员在浅拷贝时并没有被克隆,而是传递引用。而这两类正好是拷贝概念中的特例:不可变类,故详细说明一下。
首先,不可变类(Immutable Class) 在初始化后的所有信息不能被修改。而更改String成员的值相当于在堆中新建一个String引用,旧的String常量引用仍然存在于堆中,只不过还没回收,从而让两个引用不相等。这时你以为String是被克隆,但造成其不相等的原因其实是赋值。
Interger同理,也是不可变类,且在赋值时会被JVM进行**自动装箱,**从而创建新的对象引用。
在上面的例子中,其中成员son是引用类型,所以aClone克隆后存的是原son的引用。改了son的String成员,对于aClone来说其son的String一样被改了,引用是连着的而已。从而有了题目中的结果:
(aClone.son.value == a.son.value) // true
但在a本身的String成员在被改值后,发现aClone的String并没有被修改,这才是因为String的常量特性,导致a的String成员自己持有了另一个引用(如一开始所说的,赋值后堆里是两个String对象,但旧引用在克隆时就已经决定下来了)所以:
(aClone.value == a.value) // false
而对于Integer类型来说,你赋值的时候只能通过 自动装箱,等同于 new Integer(2),所以这完全是另一个对象的引用了(如上图)。所以无论是int还是Integer ,无论是基础类型还是带自动拆装箱的基础类型封装类,重新赋值都代表着成员持有新的对象引用。故:
(aClone.id == a.id) // false
此外,String的赋值本身也算一种装箱,如 str = “abc” 等同于 str = new String(“abc”) 。而对于其他引用类型来说,浅拷贝也都只是进行引用传递,不过如题这种多层引用结构,推荐直接用深拷贝。
补充一点:浅拷贝之所以仅拷贝基础类型的值,本质是因为Java对于基础类型与引用类型存储方式的不同。基础类型的值被直接存处于方法栈中,等号赋值可以直接更改栈中的值;而引用类型的真实对象存储于内存堆中,等号赋值只能更改方法栈中的内存引用地址,而动不了内存堆分毫。所以浅拷贝也只能将他们区别对待。
另一方面,String,Integer 这种不可变类在 深拷贝 中也是特例。如果你实现了深拷贝,你会发现这两类成员依然不会创建克隆,而是在深拷贝时传递引用。
看似这操作违反了Java的深拷贝定义,但它们其实完全不需要也不能被深拷贝。
最主要的原因是:String,Integer等包装类本没有实现Cloneable接口,故根本无法克隆,只能传递引用。我们自己重写clone方法时,对于String、Integer这些成员也不必去理会。
它这么设计也是因为不可变类无法更改只能新建所导致的。新建后的成员和克隆出的原成员之间本身也不会产生任何关联,那何必实现Cloneable? 我们完全没必要让堆中同时存在两个一样的不可变常量来占用双倍内存。
所以总体而言,与不可变类相关的类都比较特殊。只是在浅拷贝时需要额外注意:如明确需要共享某些引用中的数据时,要小心基础类和不可变类的数据不同步问题。
**解决方案:**让需要共享同一个值的基础类型或不可变类型成员统一指向静态常量来控制,从而无需担心数据无法同步更新的问题。
最后,关于如何自己实现一个不可变类:
① 将类声明为final
② 将所有的成员声明为private
③ 不提供setter方法
④ 将所有可变的成员声明为final
⑤ 通过构造器初始化所有成员,进行深拷贝
⑥ 在getter方法中,不直接返回对象本身,而是克隆对象,并返回对象的拷贝
和什么时候需要我们使用自定义的不可变类:
- 作为HashMap的Key时,用不可变类的对象最佳