深拷贝与浅拷贝初探
先说说浅拷贝...
一个类的拷贝构造方法通常实现为成员变量逐域赋值,即将当前对象的各个成员变量赋值为实际参数对应的各个成员变量的 值 ,称为浅拷贝。
浅拷贝有什么弱点呢?
① 当成员变量的数据类型是基本数据类型时,(int float double char boolean)
浅拷贝能够实现对象的复制功能,这个复制是完整的,没有隐患的。
② 当成员变量是引用数据类型时,浅拷贝仅仅只是复制了 这一层引用关系。
而并没有实现对象的复制功能,这个复制是有隐患的。
TheClass(SomeClass ptr_obj) //拷贝构造方法
{
this.MemberVar = ptr_obj.MemberVar;
//逐个域赋值成员变量 初始化当前实例
}
这个隐患在于:当两个引用同时指向一个对象时,例如当 SeqList1 的成员数组 element1,
经过浅拷贝后的对象 SeqList2 的成员数组变量 element2,这两个引用变量引用的是同一个数组。
当在进行 element1 的数组元素的修改时(修改包括:插入、删除...),
实际上也影响了 element2 的元素,但是并没有任何代码去更改 element2 的长度。
public SeqList(SeqList<T>list){
thsi.n = list.n;
this.element = list.element; //两者引用同一个数组
}
下面我们举个例子:
String[] values = {"a","b","c","d"};
SeqList<String> list1 = new SeqList<String>(values); //正常地通过传入数组初始化
SeqList<String> list2 = new SeqList<String>(list1); //通过拷贝方法来初始化
list1.remove(0);
System.out.println("list1 :" + list1.toString());
System.out.println("list2 :" + list2.toString());
因为删除了数组的第一项,但 list2 自己并不知晓这一点,依然在 toString() 时会输出 element[0] 这一项,所以运行时在输出 list2 时会报错:
list1 : {b,c,d}
Exception in thread "main" java.lang.NullPointerException
所以为了消除这种隐患,我们需要深拷贝。
再来说说深拷贝...
看了上面浅拷贝的同学们一定心里有数,浅拷贝的弱点就是没有实际拷贝 引用指向的那个对象或是变量,究其原因就是因为没有 新开辟内存空间来存放一个复制得到的新对象。
所以深拷贝不仅要做到浅拷贝里做到的拷贝对象的各个成员变量的值,还要为引用变量新开辟内存空间,并复制其中的所有元素或是对象。
但是 深拷贝中有一个很常见的雷区!
如下我贴出了一个并不正确的深拷贝构造方法:
public SeqList(SeqList<? extends T>list){
this.n = list.n; //基础类型变量用 赋值 来 复制
this.element = new Object[list.element.length]; //开辟空间
// 但这个空间并没有任何实际意义,因为池子里什么也没有
for(int i=0; i<list.n; i++){
this.element[i] = list.element[i];
// 其实拷贝的还是引用关系,别忘了 C/C++ 和 java 中
// xxx[yyy] 表示的仍是一个指针引用关系,其实还是一个地址指针/引用
// 引用的是 list.element 对应的数组元素。
}
}
SeqList<StringBuffer>lista = new SeqList<StringBuffer>(n-1);
for(int i=0; i<n; ++i){
lista.insert(new StringBuffer((char)('A'+i)+""));
}
// 通过 假的"深拷贝" 构造:
SeqList<StringBuffer>listb = new SeqList<StringBuffer>(lista);
lista.insert(new StringBuffer("F"));
listb.remove(listb.size()-1);
// 插入和删除 改变的只是添一个指针和砍掉一个指针而已
// 所以 lista 的改变对 listb 没有影响
StringBuffer strbuf = lista.get(0);
strbuf.setCharAt(0,'X');
lista.set(0,strbuf);
// 涉及到了对象引用的元素的实例值。
此时,lista 和 listb 引用的是同一个池子中(同一块内存地址)。
这一组元素实例值 = {a,b,c,d}
假如此时,将 lista[0] 修改为 "x",仍然会影响到 listb.
最终...
这里有一份比较直观的图例:
从此 list1 和 list2 就是完全独立,不相干的两个变量了。
当对数组元素做修改时,他们修改的都是自己储存空间里放的那一列元素。
可具体要怎么做才能实现深拷贝呢?
我们有所有类的爸爸 Object 呀!
它有11个方法,有两个protected的方法,其中一个为clone方法。
该方法声明如下:
protected native Object clone() throws CloneNotSupportedException;
因为每个类直接或间接的父类都是Object,因此它们都含有clone()方法,但是因为该方法是protected,所以都不能在类外进行访问。
要想对一个对象进行复制,就需要对clone方法覆盖。
@Override
public Object clone() {
SeqList toList = null;
try{
toList = (SeqList)super.clone();
// 这里用到了 对父类的向下转型
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return toList;
}
总结
所以深拷贝应该是,复制每个引用类型成员变量所引用的数组或是对象,
直到该对象能达到的所有对象及实例。
深拷贝/浅拷贝看上去是一个非常小非常细的知识点,但是其实能够体现很多
类、对象和方法的基础思想。
这是我第一次系统性的总结一个知识点,希望以后能做得更好。
具体针对clone方法,我们还有一大堆可以说的。
敬请期待下回分解。