至于Java的深层和灯光拷贝,简单来说就是创建一个与已知对象相似的对象。 虽然在日常编码中可能很少使用,但这是面试中经常听到的问题。 另外,了解深拷贝和写拷贝的原理可以使您更深入地了解Java中的所谓值传递和引用传递。
1、创建对象的五种方法
、使用new关键字
这是最常见的方法,使用new关键字调用具有类参数或没有参数的方法来创建对象。 例如Object obj=new Object ();
、用Class类的newInstance ()方法
此缺省值是调用类的无参数生成方法来创建对象。 例如,personP2=(person ) class.forname (com.ys.test.person ).newInstance );
、基于构造函数系统的新实例方法
这和第二种方法都是通过反射来实现的。 在java.lang.relect.Constructor类的newInstance ()方法中通过指定构造函数创建对象。
personP3=(person ) Person.class.getConstructors () [0].newInstance );
实际上,第二种方法使用Class的newInstance ) )方法创建对象。 其内部调用是构造器的新实例)方法。
、利用克隆法
Clone是Object类的方法之一,使用对象A.clone () )方法可以创建对象b,从而创建与对象名称完全一样的对象。
personP4=(person ) p3.clone );
、反序列化
序列化是指以某种方式将堆内存中的Java对象数据保存到磁盘文件中,传递给其他网络节点(在网络上传输)。 反序列化是将磁盘文件中的对象数据或网络节点上的对象数据恢复为Java对象模型的过程。
具体如何实现请参考我的这篇博文。
3、克隆方法
本博客介绍Java的深拷贝和浅拷贝。 其实现方法是通过调用Object类的clone ()方法来完成的。 在Object.class类中,源代码如下:
保护性本机对象克隆() throwsclonenotsupportedexception;
这是用native关键字修饰的方法。 有一个博客专门介绍了native关键字。 不理解也没关系。 用朴素修饰的方法是教操作系统。 这个方法我不会实现。 让操作系统实现。 不需要知道具体怎么实现。 clone方法的作用是复制对象并生成新对象。 那么,这个新对象和原对象是什么关系?
4、基本类型和参照类型
现在让我再推广一个概念。 在Java中,基本类型和引用类型是不同的。
在Java中,数据类型分为两类:基本类型和引用类型。
基本类型也称为值类型,分别为字符类型char、布尔型布尔型boolean和数值类型byte、short、int、long、float、double。
引用类型包括类、接口、数组和枚举。
Java将内存空间分为堆和堆栈。 基本类型直接在堆栈中存储数字,而引用类型将引用放在堆栈中,实际存储的值放在堆栈中,通过堆栈中的引用指向存储在堆栈中的数据。
上图定义的a和b都是基本类型,其值直接存储在堆栈中; 另一方面,c和d是在String中声明的。 这是引用类型,引用地址存储在堆栈中,指向堆的内存区域。
下一个d=c; 此语句意味着,如果将对c的引用分配给d,则c和d指向同一堆内存空间。
5、浅拷贝
看看下面的代码。
packagecom.ys.test; publicclasspersonimplementscloneable {公共字符串名称; 公共集成; 公共地址地址; public person (} public person (字符串pname,intpage ) ) {this.pname=pname; this.page=page; this.address=newAddress (;
}
@ overrideprotectedobjectclone (throwsclonenotsupportedexception ) returnsuper.clone );
} publicvoidsetaddress (stringprovices,String city ) )
address.set
Address(provices, city);}public voiddisplay(String name){
System.out.println(name+":"+"pname=" + pname + ", page=" + page +","+address);
}publicString getPname() {returnpname;
}public voidsetPname(String pname) {this.pname =pname;
}public intgetPage() {returnpage;
}public void setPage(intpage) {this.page =page;
}
}
View Code
packagecom.ys.test;public classAddress {privateString provices;privateString city;public voidsetAddress(String provices,String city){this.provices =provices;this.city =city;
}
@OverridepublicString toString() {return "Address [provices=" + provices + ", city=" + city + "]";
}
}
View Code
这是一个我们要进行赋值的原始类 Person。下面我们产生一个 Person 对象,并调用其 clone 方法复制一个新的对象。
注意:调用对象的 clone 方法,必须要让类实现 Cloneable 接口,并且覆写 clone 方法。
测试:
@Testpublic void testShallowClone() throwsException{
Person p1= new Person("zhangsan",21);
p1.setAddress("湖北省", "武汉市");
Person p2=(Person) p1.clone();
System.out.println("p1:"+p1);
System.out.println("p1.getPname:"+p1.getPname().hashCode());
System.out.println("p2:"+p2);
System.out.println("p2.getPname:"+p2.getPname().hashCode());
p1.display("p1");
p2.display("p2");
p2.setAddress("湖北省", "荆州市");
System.out.println("将复制之后的对象地址修改:");
p1.display("p1");
p2.display("p2");
}
View Code
打印结果为:
首先看原始类 Person 实现 Cloneable 接口,并且覆写 clone 方法,它还有三个属性,一个引用类型 String定义的 pname,一个基本类型 int定义的 page,还有一个引用类型 Address ,这是一个自定义类,这个类也包含两个属性 pprovices 和 city 。
接着看测试内容,首先我们创建一个Person 类的对象 p1,其pname 为zhangsan,page为21,地址类 Address 两个属性为 湖北省和武汉市。接着我们调用 clone() 方法复制另一个对象 p2,接着打印这两个对象的内容。
从第 1 行和第 3 行打印结果:
p1:com.ys.test.Person@349319f9
p2:com.ys.test.Person@258e4566
可以看出这是两个不同的对象。
从第 5 行和第 6 行打印的对象内容看,原对象 p1 和克隆出来的对象 p2 内容完全相同。
代码中我们只是更改了克隆对象 p2 的属性 Address 为湖北省荆州市(原对象 p1 是湖北省武汉市) ,但是从第 7 行和第 8 行打印结果来看,原对象 p1 和克隆对象 p2 的 Address 属性都被修改了。
也就是说对象 Person 的属性 Address,经过 clone 之后,其实只是复制了其引用,他们指向的还是同一块堆内存空间,当修改其中一个对象的属性 Address,另一个也会跟着变化。
浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。
6、深拷贝
弄清楚了浅拷贝,那么深拷贝就很容易理解了。
深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。积极的可乐修改其中一个对象的任何内容时,都不会影响另一个对象的内容。
那么该如何实现深拷贝呢?Object 类提供的 clone 是只能实现 浅拷贝的。
7、如何实现深拷贝?
深拷贝的原理我们知道了,就是要让原始对象和克隆之后的对象所具有的引用类型属性不是指向同一块堆内存,这里有三种实现思路。
①、让每个引用类型属性内部都重写clone() 方法
既然引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Person 类有一个引用类型 Address(其实String 也是引用类型,但是String类型有点特殊,后面会详细讲解),我们在 Address 类内部也重写 clone 方法。如下:
Address.class:
1 packagecom.ys.test;2
3 public class Address implementsCloneable{4 privateString provices;5 privateString city;6 public voidsetAddress(String provices,String city){7 this.provices =provices;8 this.city =city;9 }10 @Override11 publicString toString() {12 return "Address [provices=" + provices + ", city=" + city + "]";13 }14 @Override15 protected Object clone() throwsCloneNotSupportedException {16 return super.clone();17 }18
19 }
View Code
Person.class 的 clone() 方法:
1 @Override2 protected Object clone() throwsCloneNotSupportedException {3 Person p = (Person) super.clone();4 p.address =(Address) address.clone();5 returnp;6 }
View Code
测试还是和上面一样,我们会发现更改了p2对象的Address属性,p1 对象的 Address 属性并没有变化。
但是这种做法有个弊端,这里我们Person 类只有一个 Address 引用类型,而 Address 类没有,所以我们只用重写 Address 类的clone 方法,但是如果 Address 类也存在一个引用类型,那么我们也要重写其clone 方法,这样下去,有多少个引用类型,我们就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。
②、利用序列化
序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在 JVM 中,所以我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。
注意每个需要序列化的类都要实现 Serializable 接口,如果有某个属性不需要序列化,可以将其声明为 transient,即将其排除在克隆属性之外。
//深度拷贝
public Object deepClone() throwsException{//序列化
ByteArrayOutputStream bos = newByteArrayOutputStream();
ObjectOutputStream oos= newObjectOutputStream(bos);
oos.writeObject(this);//反序列化
ByteArrayInputStream bis = newByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois= newObjectInputStream(bis);returnois.readObject();
}
View Code
因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。