- Cloneable接口
- 浅拷贝
- 深拷贝
什么叫对象的拷贝? 或者克隆? 就是把这个对象完完整整的复制一份出来,两份对象是一模一样的,比双胞胎还双胞胎,那怎么样实现呢?
public static void main(String[] args) {
Person person1 = new Person("zhangsan", 18);
Person person2 = new Person("zhangsan", 18);
}
我们发现,这种写法,new出来的两个对象也是一模一样的,这种算是拷贝吗? 注意,这不叫拷贝。person2并不是person1拷贝而来的,是你从零生造出来的,而不是以person1为模板拷贝。
或者,也有人这样写:
public static void main(String[] args) {
Person person1 = new Person("zhangsan", 18);
Person person2 = person1;
}
这种也不是拷贝,这种写法呢person1和person2存储了同一块地址,都指向一个同一个对象,所以如果通过person2来修改对象中的值,同样person1的值也会改变。
public static void main(String[] args) {
Person person1 = new Person("zhangsan", 18);
Person person2 = person1;
//修改person2的属性
person2.age = 20;
System.out.println(person2.age);
System.out.println(person1.age);
}
那如何实现拷贝呢?
Cloneable接口Object 类中存在一个 clone()
方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法, 必须要先实现Cloneable
接口,重写Object的clone()方法, 否则就会抛出 CloneNotSupportedException
异常。
Cloneable是一个空接口,里面没有内容,我们叫它标记接口,代表这个类是可以被拷贝的。
然后,由person1对象调用clone方法,就会在堆上完完整整地克隆一份对象,并返回这个对象的引用。
我们打开万能的Generate,选择Override Methods,重写clone()方法。
我们来看看super的clone方法,按住Ctrl
,点进去。
我们发现它是一个native修饰的方法,它的底层是用C/C++实现的拷贝,我们看不到它的源码。我猜测可能是用了memcpy之类的方法对内存进行拷贝,不用细究。
native 方法是非 Java 语言实现的,是Java底层代码,供 Java 程序调用的。因为 Java 程序是运行在 JVM 虚拟机上面的,要想访问到比较底层的与 *** 作系统相关的就没办法了,只能由靠近 *** 作系统的语言来实现。clone就是一种对堆栈的 *** 作,一种底层的 *** 作。
由于clone方法的返回类型是Object类型的引用,我们在使用时,得用强转进行向下转型,因为父类可以直接接受子类引用(向上转型),而子类接受父类引用必须进行强转才行。如下所示:
这是因为clone方法声明了一个异常 CloneNotSupportedException
,在没有实现Cloneable接口时会抛出,我们并没有处理它,处理办法是用try-catch
语句,try关键字可以对可能出现异常的代码进行监视,如果出现异常就抛出,然后由catch关键字捕捉相应的异常进行处理。后面异常部分再细说。
class Person implements Cloneable{
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Test {
public static void main(String[] args) {
Person person1 = new Person("zhangsan", 18);
try {
Person person2 = (Person)person1.clone();
//克隆一份person1对象,由person2接受
System.out.println("person1: "+person1);
System.out.println("person2: "+person2);
} catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
运行结果:
我们来分析一下:
clone方法在堆中开辟空间,把person1中的数据拷贝进去,并把这块新的空间的内存地址返回,交给person2,就得到了我们的新的person2对象。
我们就完成了对象的拷贝,这样如果我们通过person2修改对象的属性,就不会影响person1了。
public static void main(String[] args) {
Person person1 = new Person("zhangsan", 18);
try {
Person person2 = (Person)person1.clone();
System.out.println("person1: "+person1);
System.out.println("person2: "+person2);
person2.age = 20;
System.out.println("==========修改后=========");
System.out.println("person1: "+person1);
System.out.println("person2: "+person2);
} catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
浅拷贝
这是我们基本数据类型的拷贝,如果,我们类的属性中存在引用类型变量呢?
这里String name虽然也是引用类型,但暂不涉及String深拷贝,String是被final修饰的,name所指向的对象不能被修改,name变量本身是可以被修改的。而且’‘zhangsan’'字符串会被放在常量池,后面创建对象不会多次为对象开辟空间,在详解String时再谈吧。
如这段代码所示:
我们说拷贝时开辟一块新的空间,把原来对象的所有属性的值存进去。
那现在对person1的拷贝就成了这样:
把m引用里存的地址也拷贝了一份到person2里,那么,person2的m也引用person1的m对象。
那么理所应当,我们通过person2的m来修改money,person1的money也会改变。
public static void main(String[] args) {
Person person1 = new Person("zhangsan", 18);
try {
Person person2 = (Person)person1.clone();
System.out.println("person1: "+person1);
System.out.println("person2: "+person2);
//修改年龄
person2.age = 20;
System.out.println("==========修改年龄后=========");
System.out.println("person1: "+person1);
System.out.println("person2: "+person2);
//修改money
person2.m.money = 999;
System.out.println("==========修改money后========");
System.out.println("person1: "+person1);
System.out.println("person2: "+person2);
} catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
为了打印出我们的money,我们修改一下Person的toString方法,加入money的打印。
看运行结果:
果然,person1的money也被修改成了999,那这里呢其实就是发生了所谓的浅拷贝,那应该如何避免这种问题呢?我们说需要进行深拷贝。
是不是应该把Money对象也在内存中重新拷贝一份,而不是person1和person2共用一个Money对象。
深拷贝要实现m对象的拷贝,是不是Money也得是能够拷贝的。 所以,Money类也得实现Cloneable接口,并且重写clone方法。
class Money implements Cloneable{
public int money = 20;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
我们实现好了Cloneable接口,那具体怎么实现深拷贝呢?
因为在克隆person1时,我们是通过调用Person的clone方法来实现的,所以,为了实现深拷贝,我们需要改动一下Person的clone方法,让他也能同时拷贝m对象。
@Override
protected Object clone() throws CloneNotSupportedException {
//先定义一个临时对象,让它完成浅拷贝
Person tmp = (Person)super.clone();
//然后让这个临时对象的m也拷贝一份
//这样m引用的地址就是一块新的内存空间地址
//而不是之前person1的money的地址
tmp.m = (Money)this.m.clone();//m为Money类型,所以强转为Money
//把这个临时对象返回
//返回的tmp是Person类型,发生向上转型转为Object进行返回
return tmp;
//return super.clone();
}
同样是刚刚那串代码,我们来看运行结果:
public static void main(String[] args) {
Person person1 = new Person("zhangsan", 18);
try {
Person person2 = (Person)person1.clone();
System.out.println("person1: "+person1);
System.out.println("person2: "+person2);
//修改年龄
person2.age = 20;
System.out.println("==========修改年龄后=========");
System.out.println("person1: "+person1);
System.out.println("person2: "+person2);
//修改money
person2.m.money = 999;
System.out.println("==========修改money后========");
System.out.println("person1: "+person1);
System.out.println("person2: "+person2);
} catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
我们来在内存上分析代码。
第一步:
第二步:
第三步:
至此,我们便完成了深拷贝的实现。
总结一下,关于什么是深拷贝
- 跟拷贝的数据类型有关
- 跟实际代码有关
对对象中引用类型的变量的拷贝,不能单纯的拷贝引用的值(也就是地址),必须连同引用所指向的对象一同拷贝一份。
码字不易,点个赞再走吧,收藏不迷路~
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)