我们在程序设计里,经常会遇到对象拷贝的情况,同样在拷贝的场景下,我们也会遇到“对象类型不确定”这样的情况——我们如何在不知道对象内部细节的前提下来实现拷贝对象的动作呢?
Java语言对此提供了功能强大的clone方法,专门来应对这种“对象类型不确定”的情况。不过我们在使用clone方法时,要充分理解,在对象里究竟是存储数值还是存储这个数值的引用,这个问题的答案将是我们理解深浅拷贝的钥匙,通过这把钥匙我们可以更清晰地理解Java里的数据存储和调用方式。
1 “偷懒”的共享数据块的方法——浅拷贝
如果我们把多个句柄同时指向同一个对象,那么多个句柄可以共享一个对象(或称是内存数据块),通过下面的ShareObject.java代码,我们可以看出共享数据块的一些特点。
class ShareData
{
private int data;
//构造函数
ShareData(int data)
{
this.data = data;
}
//输出
void printData()
{
System.out.println("the data is " + data);
}
//自增长data的方法
void increaseData()
{
data++;
}
}
public class ShareObject
{
public static void main(String[] args)
{
ShareData sd1 = new ShareData(1);
//共享内存块
ShareData sd2 = sd1;
System.out.println("Sharedata2.");
sd2.printData();
//调用sd2的increaseData方法
sd2.increaseData();
//检验结果
System.out.println("after increase Sharedata2.");
System.out.println("this is Sharedata2.");
sd2.printData();
System.out.println("this is Sharedata1.");
sd1.printData();
}
}
这段代码的输出的结果是:
this is Sharedata2.
the data is 1
after increase Sharedata2.
this is Sharedata2.
the data is 2
this is Sharedata1.
the data is 2
其中我们看到一个非常有意思的现象:最初我们初始化了ShareData类型的sd1对象,然后我们通过ShareData sd2 = sd1;语句,把sd1的存储空间的首地址(即引用)赋予了sd2,这样,就相当于两个句柄同时指向了同一个内存里的ShareData对象。
接下来,虽然我们是通过sd2.increaseData();语句,仅仅提升了sd2里的data值,但通过输出结果,我们看到了,sd1内的数据也受到了影响。
我们在实际的项目开发中,不提倡这种做法,因为如果用两个句柄同时操作一个对象的话,很有可能导致数据被意外地修改掉。
所以,如果在不同的场景下,需要对同一个对象进行操作,我们一般是调用Java里的clone方法,让不同的场景代码针对克隆出来的不同对象来操作。
2 似是而非的浅拷贝——只拷贝ArrayList对象
在Java的Object基类里,提供了用于对象复制的clone方法,由于Java里,所有类都是Object的子类,所以每个类都具有通过调用clone方法克隆自身的能力。
但是,clone方法的一些表现也会出乎我们的意料,在下面的ShallowClone.java代码里,虽然我们调用了clone,但发现并不能真正地克隆在ArrayList里的数据。
import java.util.ArrayList;
import java.util.List;
class Item
{
int data;
public Item(int data)
{
this.data = data;
}
public String toString()
{
return " "+data;
}
}
public class ShallowClone
{
static void prt(List l)
{
for(int i=0;i<l.size();i++)System.out.print(l.get(i));
}
public static void main(String[] args)
{
ArrayList l = new ArrayList();
for (int i = 0; i < 10; i++)
{
l.add(new Item(i));
}
System.out.println("at begin:");
prt(l);
//克隆l里面的数据
List l2 = (List) l.clone();
//改变l2中的数据,但影响了l中的数据
((Item)l2.get(0)).data++;
System.out.println("/nafter clone and adjust l2:");
prt(l);
System.out.println();
prt(l2);
}
}
这段程序直接调用了集合ArrayList实现了克隆,虽然我们操作的动作针对的是l2对象,但修改l2中的元素会影响到ll中原来的数据。程序输出结果如下:
at begin:
0 1 2 3 4 5 6 7 8 9
after clone and adjust l2:
1 1 2 3 4 5 6 7 8 9
1 1 2 3 4 5 6 7 8 9
这种做法叫“浅拷贝”,因为我们只能拷贝ArrayList对象,而无法继续拷贝包含在对象里的 元素。
3 “刨根撅底”的深拷贝——实现对ArrayList的整体克隆
在上面的例子里,我们之所以没能够达到预期的ArrayList整体克隆的效果,是因为我们在克隆ArrayList对象时,仅仅克隆了ArrayList本身,而没有克隆里面具体的对象。如果自定义对象想要实现克隆的特性的话,只需实现Clonalbe接口,并且实现clone方法,这个方法是来自于java.lang.Object这个父类的。下面我们看一下修改后的代码DeepClone.java。
import java.util.ArrayList;
import java.util.List;
class Item1 implements Cloneable
{
int data;
public Item1(int data)
{
this.data = data;
}
public String toString()
{
return " "+data;
}
public Object clone()
{
Object o=null;
try
{
o = super.clone();
}
catch(CloneNotSupportedException e)
{
System.err.println("wrong in clone");
}
return o;
}
}
public class DeepClone
{
static void prt(List l)
{
for(int i=0;i<l.size();i++)
{
System.out.print(l.get(i));
}
}
public static void main(String[] args)
{
ArrayList l = new ArrayList();
for (int i = 0; i < 10; i++)
{
l.add(new Item1(i));
}
System.out.println("at begin:");
prt(l);
//克隆l里面的数据
List l2 = (List) l.clone();
//深度克隆l中的所有元素
for(int i=0;i<l2.size();i++)
{
l2.set(i,((Item1)l.get(i)).clone());
}
//改变l2中的数据,不会影响l中的数据
((Item1)l2.get(0)).data++;
System.out.println("/nafter clone and adjust l2:");
prt(l);
System.out.println();
prt(l2);
}
}
上述代码不仅对ArrayList本身进行了克隆,对它里面的所有元素也采用了克隆。这样就能保证克隆之后的两个集合在数据上是一致的但却是两个完全独立的对象,互相之间没有任何共享的数据。该程序输出结果如下:
at begin:
0 1 2 3 4 5 6 7 8 9
after clone and adjust l2:
0 1 2 3 4 5 6 7 8 9
1 1 2 3 4 5 6 7 8 9
请大家注意,区分深浅拷贝的关键是,在深拷贝的做法里,是通过覆盖了clone方法,继续定义了复制集合内元素的方法。