我们自定义的类是以引用的形式放入集合, 如果使用不当, 会引发非常隐蔽的错误就拿我经常问到的一个面试题来说明这个知识点
第一步, 我们定义一个 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 来分析这个问题:
- import java.util.ArrayList;
- // 第一步, 创建一个 Car 类, 其中只有一个属性 i
- // 通过构造函数, 我们可以设置 i 的值, 而且有针对 i 的 get 和 set 方法
- class Car {
- private int i;
- public int getI() {return i;}
- public void setI(int i) {this.i = i; }
- public Car(int i) {this.i = i; }
- }
- public class CopyDemo {
- public static void main(String[] args) {
- // 第二步, 创建一个 Car 的实例, 其中的 id 是 1
- Car c = new Car(1);
- // 第三步, 创建两个不同的 ArrayList
- ArrayList<Car> a1 = new ArrayList<Car>();
- ArrayList<Car> a2 = new ArrayList<Car>();
- // 通过 add 方法把 c 分别加入到 a1 和 a2 里
- a1.add(c);
- a2.add(c);
- // 第四步, 修改 a1 中的 c 对象, 但不修改 a2 中的 c 对象
- a1.get(0).setI(2);
- // 最后通过打印查看 a2 里 c 对象的 id
- System.out.println(a2.get(0).getI());
- }
- }
根据第 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 这个例子
- import java.util.ArrayList;
- // 实现 Cloneable 接口, 重写其中的 clone 方法
- class CarForDeepCopy implements Cloneable {
- private int i;
- public int getI() {return i;}
- public void setI(int i) {this.i = i;}
- // 构造函数
- public CarForDeepCopy(int i)
- {this.i = i;}
- // 调用父类的 clone 完成对象的拷贝
- public Object clone() throws CloneNotSupportedException
- { return super.clone(); }
- }
- public class DeepCopy {
- public static void main(String[] args) {
- CarForDeepCopy c1 = new CarForDeepCopy(1);
- // 通过 clone 方法把 c1 做个备份
- CarForDeepCopy c2 = null;
- try {
- c2 = (CarForDeepCopy)c1.clone();
- } catch (CloneNotSupportedException e) {
- e.printStackTrace();
- }
- ArrayList<CarForDeepCopy> a1 = new ArrayList<CarForDeepCopy>();
- ArrayList<CarForDeepCopy> a2 = new ArrayList<CarForDeepCopy>();
- a1.add(c1);
- a2.add(c2);
- // 修改 a1 中的 c 对象的 id 为 2
- a1.get(0).setI(2);
- // 输出依然是 1
- System.out.println(a2.get(0).getI());
- }
- }
为了实现 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 对象
来源: https://www.cnblogs.com/JavaArchitect/p/8614961.html