创建对象的四大方法:1、new;2、反射;3、克隆;4、反序列化
今天来看一下如何克隆一个对象出来,克隆分为2种,一种是浅克隆,一种是深克隆。
一、在浅克隆中,如果原型对象的属性是值类型(如int,double,byte,boolean,char等),将复制一份给克隆对象;如果原型对象的属性是引用类型(如类,接口,数组,集合等复杂数据类型),则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的属性指向相同的内存地址。简单来说,在浅克隆中,当原型对象被复制时只复制它本身和其中包含的值类型的属性,而引用类型的属性并没有复制。
先定义一个附件类
@Data @AllArgsConstructor public class Attachment { private String name; public void download() { System.out.println("下载附件,文件名为" + name); } }
定义一个周报类
@Data public class WeeklyLog implements Cloneable { private Attachment attachment; @Override public WeeklyLog clone() { Object obj = null; try { obj = super.clone(); return (WeeklyLog) obj; } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } } }
写一个main方法来进行浅克隆
public class Client { public static void main(String[] args) { WeeklyLog log_previous = new WeeklyLog(); WeeklyLog log_new = null; Attachment attachment = new Attachment("附件"); log_previous.setAttachment(attachment); log_new = log_previous.clone(); System.out.println("周报是否相同?" + (log_previous == log_new)); System.out.println("附件是否相同?" + (log_previous.getAttachment() == log_new.getAttachment())); } }
运行结果:
周报是否相同?false
附件是否相同?true
由此可以看出log_prevlous跟log_new具有不同的内存地址,它们的附件对象内存地址相同。
二、深克隆,如果我们要修改克隆出来的对象的引用属性时就会把原型对象的该属性也同时修改掉,我们将main方法修改如下
public class Client { public static void main(String[] args) { WeeklyLog log_previous = new WeeklyLog(); WeeklyLog log_new = null; Attachment attachment = new Attachment("附件"); log_previous.setAttachment(attachment); log_new = log_previous.clone(); System.out.println("周报是否相同?" + (log_previous == log_new)); System.out.println("附件是否相同?" + (log_previous.getAttachment() == log_new.getAttachment())); System.out.println(log_previous.getAttachment().getName()); log_new.getAttachment().setName("新附件"); System.out.println(log_previous.getAttachment().getName()); } }
运行结果:
周报是否相同?false
附件是否相同?true
附件
新附件
我们可以看到我们修改了log_new,log_previous也跟着改了。
深克隆就是让克隆对象的引用属性跟原型对象没有关系,由浅克隆的特性,我们可以知道,克隆出来的对象本身与原型对象是不同的内存地址的,由此我们可以将引用类型也添加克隆的特性,这样就可以将引用类型也分离出来。
附件对象修改如下
@Data @AllArgsConstructor public class Attachment implements Cloneable { private String name; public void download() { System.out.println("下载附件,文件名为" + name); } @Override public Attachment clone() throws CloneNotSupportedException { return (Attachment)super.clone(); } }
周报对象修改如下
@Data public class WeeklyLog implements Cloneable { private Attachment attachment; @Override public WeeklyLog clone() { WeeklyLog obj = null; try { obj = (WeeklyLog) super.clone(); if (obj != null) { obj.setAttachment(obj.getAttachment().clone()); } return obj; } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } } }
我们再重新执行上述main方法
运行结果:
周报是否相同?false
附件是否相同?false
附件
附件
由此可见,深克隆后,附件对象的内存地址已经不一样了,修改了克隆对象的附件地址,原型对象并不会受到影响。
但是如果原型对象中的引用属性对象中又包含引用属性,嵌套非常多,使用该方法来进行深度克隆就会非常麻烦了,这个时候我们只能够使用序列化和反序列化来生成克隆对象了。此处我们只使用Java的序列化和反序列化来做说明。
这里所有的类都要加上序列化接口
附件类
@Data @AllArgsConstructor public class Attachment implements Cloneable,Serializable { private String name; public void download() { System.out.println("下载附件,文件名为" + name); } @Override public Attachment clone() throws CloneNotSupportedException { return (Attachment)super.clone(); } }
周报类,添加深度克隆方法deepClone()
@Data public class WeeklyLog implements Cloneable,Serializable { private Attachment attachment; @Override public WeeklyLog clone() { WeeklyLog obj = null; try { obj = (WeeklyLog) super.clone(); if (obj != null) { obj.setAttachment(obj.getAttachment().clone()); } return obj; } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } } public WeeklyLog deepClone() throws IOException, ClassNotFoundException { //将对象写入流中 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(this); //将对象从流中取出 ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); ObjectInputStream inputStream = new ObjectInputStream(arrayInputStream); return (WeeklyLog) inputStream.readObject(); } }
修改main()方法
public class Client { public static void main(String[] args) throws IOException, ClassNotFoundException { WeeklyLog log_previous = new WeeklyLog(); WeeklyLog log_new = null; Attachment attachment = new Attachment("附件"); log_previous.setAttachment(attachment); log_new = log_previous.deepClone(); System.out.println("周报是否相同?" + (log_previous == log_new)); System.out.println("附件是否相同?" + (log_previous.getAttachment() == log_new.getAttachment())); System.out.println(log_previous.getAttachment().getName()); log_new.getAttachment().setName("新附件"); System.out.println(log_previous.getAttachment().getName()); } }
运行结果:
周报是否相同?false
附件是否相同?false
附件
附件
现在我们来看一下,它中间的二进制字节码有多少,修改一下deepClone()方法,打印二进制字节码
public WeeklyLog deepClone() throws IOException, ClassNotFoundException { //将对象写入流中 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(this); //将对象从流中取出 System.out.println(Arrays.toString(byteArrayOutputStream.toByteArray())); ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); ObjectInputStream inputStream = new ObjectInputStream(arrayInputStream); return (WeeklyLog) inputStream.readObject(); }
运行上述main方法
[-84, -19, 0, 5, 115, 114, 0, 28, 99, 111, 109, 46, 103, 117, 97, 110, 106, 105, 97, 110, 46, 99, 108, 111, 110, 101, 46, 87, 101, 101, 107, 108, 121, 76, 111, 103, -10, -93, 81, -98, -8, -9, -59, 96, 2, 0, 2, 76, 0, 10, 97, 116, 116, 97, 99, 104, 109, 101, 110, 116, 116, 0, 31, 76, 99, 111, 109, 47, 103, 117, 97, 110, 106, 105, 97, 110, 47, 99, 108, 111, 110, 101, 47, 65, 116, 116, 97, 99, 104, 109, 101, 110, 116, 59, 76, 0, 4, 110, 97, 109, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 112, 115, 114, 0, 29, 99, 111, 109, 46, 103, 117, 97, 110, 106, 105, 97, 110, 46, 99, 108, 111, 110, 101, 46, 65, 116, 116, 97, 99, 104, 109, 101, 110, 116, 113, -46, -107, -3, -36, 44, 1, -94, 2, 0, 1, 76, 0, 4, 110, 97, 109, 101, 113, 0, 126, 0, 2, 120, 112, 116, 0, 6, -23, -103, -124, -28, -69, -74, 116, 0, 6, -27, -111, -88, -26, -118, -91]
周报是否相同?false
附件是否相同?false
附件
附件
我们可以看到这个二进制的字节码是非常长的,一般我们在实际开发中是不使用Java本身的序列化方式的,现在我们增加一种第三方的高效序列化工具kryo
在pom中增加依赖
<dependency> <groupId>com.esotericsoftware</groupId> <artifactId>kryo-shaded</artifactId> <version>4.0.2</version> </dependency>
因为kryo在反序列化中需要无参构造器,所以我们需要在WeeklyLog和Attachment中添加标签@NoArgsConstructor
在WeeklyLog中添加方法deepCloneByKryo()
public WeeklyLog deepCloneByKryo() { Input input = null; try { Kryo kryo = new Kryo(); ByteArrayOutputStream stream = new ByteArrayOutputStream(); Output output = new Output(stream); kryo.writeObject(output, this); output.close(); System.out.println(Arrays.toString(stream.toByteArray())); input = new Input(new ByteArrayInputStream(stream.toByteArray())); return kryo.readObject(input,WeeklyLog.class); }finally { input.close(); } }
修改main方法如下
public class Client { public static void main(String[] args) throws IOException, ClassNotFoundException { WeeklyLog log_previous = new WeeklyLog(); log_previous.setName("周报"); WeeklyLog log_new = null; Attachment attachment = new Attachment("附件"); log_previous.setAttachment(attachment); log_new = log_previous.deepCloneByKryo(); System.out.println("周报是否相同?" + (log_previous == log_new)); System.out.println("附件是否相同?" + (log_previous.getAttachment() == log_new.getAttachment())); System.out.println(log_previous.getAttachment().getName()); log_new.getAttachment().setName("新附件"); System.out.println(log_previous.getAttachment().getName()); } }
运行结果:
[1, 1, 0, 99, 111, 109, 46, 103, 117, 97, 110, 106, 105, 97, 110, 46, 99, 108, 111, 110, 101, 46, 65, 116, 116, 97, 99, 104, 109, 101, 110, -12, 1, 1, -125, -23, -103, -124, -28, -69, -74, 1, -125, -27, -111, -88, -26, -118, -91]
周报是否相同?false
附件是否相同?false
附件
附件