我们自定义的类是以引用的形式放入集合,如果使用不当,会引发非常隐蔽的错误。就拿我经常问到的一个面试题来说明这个知识点。

第一步,我们定义一个Car类型的类,其中只有一个int类型id属性。

第二步,创建一个Car类的实例,假设是c,设置它的id是1。

第三步,我们通过new关键字创建两个不同的ArrayList,分别是a1和a2,这里请注意我的描述,是创建两个不同的ArrayList,而不是一个,并把第二步里创建的c对象分别放入a1和a2。

第四步,我们在a1这个ArrayList里,拿出c对象,并把它的id设置成2,同时不对存放在a2里的c对象做任何的修改。

我的问题是,完成上述四步后,a2里存放的c对象的id是多少?1还是2?

我们通过如下的CopyDemo.java来分析这个问题:

1    import java.util.ArrayList;
2 //第一步,创建一个Car类,其中只有一个属性i
3 //通过构造函数,我们可以设置i的值,而且有针对i的get和set方法
4 class Car {
1 private int i;
2 public int getI() {return i;}
3 public void setI(int i) {this.i = i; }
4 public Car(int i) {this.i = i; }
5 }
6 public class CopyDemo {
7 public static void main(String[] args) {
8 //第二步,创建一个Car的实例,其中的id是1
9 Car c = new Car(1);
10 //第三步,创建两个不同的ArrayList
11 ArrayList<Car> a1 = new ArrayList<Car>();
12 ArrayList<Car> a2 = new ArrayList<Car>();
13 //通过add方法把c分别加入到a1和a2里
14 a1.add(c);
15 a2.add(c);
16 //第四步,修改a1中的c对象,但不修改a2中的c对象
17 a1.get(0).setI(2);
18 //最后通过打印查看a2里c对象的id
19 System.out.println(a2.get(0).getI());
20 }
21 }

根据第19行的输出,虽然我们并没对a2里存放的c对象做任何的操作,但它的值也被改成了2,根据我面试下来的结果,估计有一半的初级程序员(工作经验3年以下)会回答错。

原因是我们之前提到过的:集合里存放的是引用。下面我们来详细说明这点。

第一,当执行完Car c = new Car(1);操作后,Java虚拟机会在内存里开辟一块空间存放id是1的c对象,假设这块空间的首地址是1000,那么c其实是指向1000号空间的引用。

第二,我们其实是把c这个引用放入到两个不同的ArrayList里,大家可以通过下图来观察下效果。

从上图里我们能看到,a1和a2里第一个元素存放的其实都是c这个引用。通过这个引用,都能指向到存放在1000号内存里id是1的这个c对象。

当我们通过a1修改存放在其中的c对象时,其实是通过c这个引用直接改变了1000号里的id,由于a2里存放的引用也是指向1000号内存,所以虽然我们并没有改变过a2里的c对象,但a2里的值也跟着变了。

在实际项目里,可能会遇到类似的问题。比如我们把同一份变量放入两个不同的集合对象里,我们的本意是,在一个集合里给该变量做个备份,只在另外一个集合里修改。但根据上文的描述,即使我们只在其中一个集合里做修改,这个修改会影响到另外一个我们企图做备份的集合,这和我们想当然的结果不一样。

如果要正确地实现“一个集合做备份另一个集合做修改”的效果,我们就得通过clone方法来实现深拷贝了,来看DeepCopy.java这个例子。

1    import java.util.ArrayList;
2 //实现Cloneable接口,重写其中的clone方法
3 class CarForDeepCopy implements Cloneable {
4 private int i;
5 public int getI() {return i;}
6 public void setI(int i) {this.i = i;}
7 //构造函数
8 public CarForDeepCopy(int i)
9 {this.i = i;}
10 //调用父类的clone完成对象的拷贝
11 public Object clone() throws CloneNotSupportedException
12 { return super.clone(); }
13 }
14 public class DeepCopy {
15 public static void main(String[] args) {
16 CarForDeepCopy c1 = new CarForDeepCopy(1);
17 //通过clone方法把c1做个备份
18 CarForDeepCopy c2 = null;
19 try {
20 c2 = (CarForDeepCopy)c1.clone();
21 } catch (CloneNotSupportedException e) {
22 e.printStackTrace();
23 }
24 ArrayList<CarForDeepCopy> a1 = new ArrayList<CarForDeepCopy>();
25 ArrayList<CarForDeepCopy> a2 = new ArrayList<CarForDeepCopy>();
26 a1.add(c1);
27 a2.add(c2);
28 //修改a1中的c对象的id为2
29 a1.get(0).setI(2);
30 //输出依然是1
31 System.out.println(a2.get(0).getI());
32 }
33 }

为了实现clone,我们自定义的类必须要像第3行那样实现Cloneable接口;同时像第11行那样重写clone方法。其中我们可以像第12行那样,通过super调用父类的clone方法来完成内容的拷贝。

在第20行里,我们通过调用c1对象的clone方法在内存里创建另外一个CarForDeepCopy对象,随后当我们通过第26和27行把c1和c2放入到两个ArrayList后,在内存里存储的结构如下图所示。

从中我们能看到,c1被clone后,系统会开辟一块新的空间用以存放和c1相同的内容,并用c2指向这块内存。

这样a1和a2两个ArrayList就通过c1和c2这两个不同的引用指向了两块不同的内存空间,所以a1的修改不会影响到a2对象。

最新文章

  1. Linux(Centos)之安装Redis及注意事项
  2. Java随笔二
  3. JAVA线程同步辅助类CountDownLatch
  4. 分页pagination实现及其应用
  5. c++标准库和stl关系
  6. html5的程序接口与元素变化
  7. Linux下的paste合并命令详解
  8. SSIS 学习(6):包配置(上)【转】
  9. POJ 1637 Sightseeing tour ★混合图欧拉回路
  10. 转载 VPN介绍
  11. 树莓派学习路程No.1 树莓派系统安装与登录 更换软件源 配置wifi
  12. IIS下图片防盗连设置详解
  13. Android 加速Gradle构建项目
  14. scrapy初试水 day03(递归调用)
  15. idea svn 设置忽略 文件
  16. logstash笔记(二)——grok之match
  17. Django runserver UnicodeDecodeError
  18. AtomicInteger类和int原生类型自增鲜明的对比
  19. 12-关于DOM操作的相关案例
  20. Mac怎么安装并配置Homebrew?

热门文章

  1. PAT1117. Eddington Number
  2. uva10003 区间DP
  3. 【BZOJ1095】 Hide 捉迷藏
  4. rem是如何自适应的
  5. Web渗透测试笔记(基础部分)
  6. 1_Two Sum --LeetCode
  7. (二十二)java小练习三
  8. JSP中的编译指令和动作指令的区别
  9. 配置文件properties读取使用的好方法
  10. iOS - Quartz 2D 二维绘图