今天遇到一道面试题,询问深拷贝的两种方法。主要就是clone方法和序列化方法。今天就来分析一下这两种方式如何实现深拷贝。如果想跳过解析的朋友,直奔“重点来了!”寻找答案。

clone方法

例1:我们不妨建立一个Exam对象

考试类Exam.java文件

public class Exam implements Cloneable {

    private int examId;

    private String examName;

    public Exam() {
} public Exam(int examId, String examName) {
this.examId = examId;
this.examName = examName;
} public int getExamId() {
return examId;
} public void setExamId(int examId) {
this.examId = examId;
} public String getExamName() {
return examName;
} public void setExamName(String examName) {
this.examName = examName;
} @Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

测试类Main.java

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
Exam exam = new Exam(1, "语文考试");
Exam cloneExam = (Exam) exam.clone();
System.out.println(cloneExam != exam);
System.out.println(cloneExam.equals(exam));
}
}

控制台输出:

true

false

我们确实拷贝出了另一个对象。equals没有覆写,所以调用的是java.lang.Object中的以下方法:

public boolean equals(Object obj) {
return (this == obj);
}

例2:假如我们给考试加个监考老师

老师类Teacher.java,不实现Cloneable接口

public class Teacher {

    private String name;

    public Teacher(String name) {
this.name = name;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
}
}

把老师对象作为属性新增到考试类Exam.java中(设置监考老师)

public class Exam implements Cloneable {

    private int examId;

    private String examName;

    private Teacher teacher;

    public Exam() {
} public Exam(int examId, String examName) {
this.examId = examId;
this.examName = examName;
} public int getExamId() {
return examId;
} public void setExamId(int examId) {
this.examId = examId;
} public String getExamName() {
return examName;
} public void setExamName(String examName) {
this.examName = examName;
} public Teacher getTeacher() {
return teacher;
} public void setTeacher(Teacher teacher) {
this.teacher = teacher;
} @Override
public String toString() {
return "Exam{" +
"examId=" + examId +
", examName='" + examName + '\'' +
", teacher=" + teacher +
'}';
} @Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

改写测试类Main.java

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {
Exam exam = new Exam(1, "语文考试");
Teacher teacher = new Teacher("马老师");
exam.setTeacher(teacher);
Exam cloneExam = (Exam) exam.clone();
System.out.println(cloneExam != exam);
System.out.println(cloneExam.equals(exam)); cloneExam.getTeacher().setName("Lily");
System.out.println(exam.toString());
System.out.println(cloneExam.toString());
}
}

控制台输出:

true

false

Exam{examId=1, examName='语文考试', teacher=Teacher{name='Lily'}}

Exam{examId=1, examName='语文考试', teacher=Teacher{name='Lily'}}

相信眼尖的朋友已经发现端倪了,详细的分析可见下文“clone方法的存在问题”

clone方法总结:

调用clone方法的前提:

  1. Exam需要继承java.lang.Cloneable接口。否则代码在运行时报错。

解释:

调用exam.clone()的对象类Exam需要继承Cloneable接口,否则会在代码运行时抛出CloneNotSupportedException异常

  1. Exam需要覆写父类的clone()方法。否则代码在编译时报错。

解释:

因为clone()java.lang.Object中是protected访问控制。如果不覆写,exam.clone()这句代码无法编译通过

clone方法的存在问题:

我们从上述例2中结果中发现,我原本只想将克隆出来的考试的监考老师改为 Lily ,但是把原考试对象的监考老师也修改了,这就十分尴尬了。

阅读java.lang.Object中的clone()方法上的英文注释时有这样一段话:

*** this method creates a new instance of the class of this object and initializes all its fields with exactly the contents of the corresponding fields of this object, as if by assignment; the contents of the fields are not themselves cloned. Thus, this method performs a "shallow copy" of this object, not a "deep copy" operation. ***

翻译为:

该方法创建该对象类的新实例,并使用该对象相应字段的内容完全初始化其所有字段,就像通过赋值一样; 字段的内容本身不会被克隆。 因此,此方法执行此对象的“浅复制”,而不是“深复制”操作。

重点来了!使用clone方式实现“深拷贝”

覆写考试类Exam.javaclone()方法

@Override
protected Object clone() throws CloneNotSupportedException {
Exam exam = (Exam) super.clone();
if (teacher != null) {
Teacher teacher = (Teacher) this.teacher.clone();
exam.setTeacher(teacher);
}
return exam;
}

解析

用上述方法,取代return super.clone()的默认实现。同时因为这里调用了teacher.clone(),所以类Teacher也要实现Cloneable接口,覆写clone()方法。

改写老师类Teacher.java

public class Teacher implements Cloneable{

    private String name;

    public Teacher() {
} public Teacher(String name) {
this.name = name;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} @Override
public String toString() {
return "Teacher{" +
"name='" + name + '\'' +
'}';
} @Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

控制台输出:

true

false

Exam{examId=1, examName='语文考试', teacher=Teacher{name='马老师'}}

Exam{examId=1, examName='语文考试', teacher=Teacher{name='Lily'}}

序列化方法

每个对象覆写Cloneable方法也是够麻烦的,接下来的介绍的序列化方法更为简洁。

原理:对象->字节数组(拷贝)->对象

提到序列化,就不得不提到java.lang.Serializable,建议好好阅读一下类上的注释。

静态的序列化“深拷贝”方法(简易版)

public class Util {
private Util() {}
public static Object deepCopy(Object exam) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bs);
os.writeObject(exam); ByteArrayInputStream bis = new ByteArrayInputStream(bs.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}

例1:考试类(无对象成员变量)

考试类对象Exam.java实现Serializable接口

public class Exam implements Serializable {

    private int examId;

    private String examName;

    public Exam() {
} public Exam(int examId, String examName) {
this.examId = examId;
this.examName = examName;
} public int getExamId() {
return examId;
} public void setExamId(int examId) {
this.examId = examId;
} public String getExamName() {
return examName;
} public void setExamName(String examName) {
this.examName = examName;
} }

测试类Main.java

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Exam exam = new Exam(1, "语文考试");
Exam copyExam = (Exam) Util.deepCopy(exam);
System.out.println(copyExam != exam);
System.out.println(copyExam.equals(exam));
}
}

控制台输出:

true

false

例2:考试类(含对象成员变量)

老师类Teacher.java

public class Teacher implements Serializable {

    private String name;

    public Teacher() {
} public Teacher(String name) {
this.name = name;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} @Override
public String toString() {
return "Teacher{" +
"name='" + name + '\'' +
'}';
} }

改写Exam.java,新增成员变量teacher

public class Exam implements Serializable {

    private int examId;

    private String examName;

    private Teacher teacher;

    public Exam() {
} public Exam(int examId, String examName) {
this.examId = examId;
this.examName = examName;
} public int getExamId() {
return examId;
} public void setExamId(int examId) {
this.examId = examId;
} public String getExamName() {
return examName;
} public void setExamName(String examName) {
this.examName = examName;
} public Teacher getTeacher() {
return teacher;
} public void setTeacher(Teacher teacher) {
this.teacher = teacher;
} @Override
public String toString() {
return "Exam{" +
"examId=" + examId +
", examName='" + examName + '\'' +
", teacher=" + teacher +
'}';
}
}

改写测试类Main.java

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Exam exam = new Exam(1, "语文考试");
Teacher teacher = new Teacher("马老师");
exam.setTeacher(teacher);
Exam copyExam = (Exam) Util.deepCopy(exam);
System.out.println(copyExam != exam);
System.out.println(copyExam.equals(exam)); copyExam.getTeacher().setName("Lily");
System.out.println(exam);
System.out.println(copyExam);
}
}

控制台输出:

true

false

Exam{examId=1, examName='语文考试', teacher=Teacher{name='马老师'}}

Exam{examId=1, examName='语文考试', teacher=Teacher{name='Lily'}}

序列化方法总结

调用deepCopy方法的前提:

  1. Exam需要实现java.lang.Serializable接口。否则代码在运行时报错。

解释:

对象类Exam需要实现java.lang.Serializable接口,否则会在代码执行到os.writeObject(exam)时抛出NotSerializableException异常。

  1. Exam中的成员变量类Teacher也需要实现java.lang.Serializable接口。否则在运行时报错。

解释:

当类Exam中包含了成员变量Teacher时,如果只有Exam实现java.lang.Serializable接口,但是Teacher没有实现java.lang.Serializable接口,那么代码执行到os.writeObject(exam)时还是会**抛出NotSerializableException异常。

重点来了!使用泛型实现序列化“深拷贝”方法

public class Util {
private Util() {}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T obj) {
T cloneObj = null;
try {
//写入字节流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close(); //分配内存,写入原始对象,生成新对象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
//返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}

使用该方法可以在代码编译期检查出没有实现java.lang.Serializable接口的对象。

总结

  1. clone()方法要求目标类及其成员变量类都需要实现java.lang.Cloneable接口,并且覆写java.lang.Objectclone()方法。
  2. 序列化方法通过静态方法实现,其目标类及其成员变量类都需要实现java.lang.Serializable接口。

一步步分析Java深拷贝的两种方式-clone和序列化的更多相关文章

  1. java笔记线程两种方式模拟电影院卖票

    public class SellTicketDemo { public static void main(String[] args) { // 创建三个线程对象 SellTicket st1 = ...

  2. 创建Java多线程的两种方式和线程异常

    一.使用多线程的两种方法  使用多线程的两种方法有:继承Thread类和实现runable接口. 二.继承Thread类 来看一下thread类的源代码: class Thread implement ...

  3. Java异常处理的两种方式以及自定义异常的使用方法

    异常 就是程序出现了不正常的情况 Error:严重问题,不需要处理 Exception:称为异常类,他表示程序本身可以处理的问题 RuntimeException:在编译期是不检查的,出现问题后,需要 ...

  4. Java 和 数据库两种方式进行加锁

    java方式: publicstatic synchronized int generate(StringtableName){ Stringsql = "select value from ...

  5. java判断数据类型两种方式

    instanceof        String s = ""; System.out.println(s instanceof String); // true     simp ...

  6. 阿里巴巴--java多线程的两种实现方式,以及二者的区别

    阿里巴巴面试的时候,昨天问了我java面试的时候实现java多线程的两种方式,以及二者的区别当时只回答了实现线程的两种方式,但是没有回答上二者的区别: java实现多线程有两种方式: 1.继承Thre ...

  7. Java多线程的两种实现方式

    Java总共有两种方式实现多线程 方式1:通过继承Thread类的方式 package com.day04; /** * 通过继承Thread类并复写run方法来是实现多线程 * * @author ...

  8. ORACLE数据库实现自增的两种方式

    Mysql数据库因为其有自动+1,故一般我们不需要花费太多时间,直接用关键字auto_increment即可,但是Oracle不行,它没有自动增长机制.顾我们需要自己去实现.一般有两种方式,但是这两种 ...

  9. java 的对象拷贝(有深浅拷贝两种方式,深拷贝实现的两种方式(逐层实现cloneable接口,序列化的方式来实现))

    Java提高篇--对象克隆(复制)(转自:http://www.cnblogs.com/Qian123/p/5710533.html#_label0)   阅读目录 为什么要克隆? 如何实现克隆 浅克 ...

随机推荐

  1. Python3基础学习-while循环实例- 猜数字游戏

    需求: 猜数字游戏, 要求如下: 给个数字, 比如是66. 让用户一直猜,直到猜对退出. 中间输入q或Q也能退出 如果猜大了,提示用户猜小一点; 如果猜小了,提示用户猜大一点; likely = 66 ...

  2. swoole组件----mysql查询,插入数据

    注意!任何swoole函数都应该包含在go(function(){}) 经典查询方法query() go(function (){ $swoole_mysql = new Swoole\Corouti ...

  3. Shiro 中的 SecurityUtils(转)

    在 Shiro 中 SecurityUtils 是一个抽象类.并且没有任何子类.在其中声明了一个静态属性,三个静态方法. 静态属性 securityManager private static Sec ...

  4. list,tuple,set,dict基础

    list # @Auther : chen # @Time : 2018/4/26 19:55 # @File : list_ex.py # @SoftWare : PyCharm # list1 = ...

  5. Oracle - 特殊字符问题

    Oracle更新表字段或者查询表字段时内容中含有特殊字符&的解决方法 现象 解决方式 1. 字符串拼接 UPDATE T_MENU_INFO SET menu_code='/ABeptjk/g ...

  6. js字符串字母大小写转换

    toLocaleUpperCase 方法 返回一个字符串,其中所有的字母字符都被转换为大写,同时适应宿主环境的当前区域设置. stringVar.tolocaleUpperCase( )必选的 str ...

  7. fastJson与jackson性能对比

    转载:https://blog.csdn.net/u013433821/article/details/82905222最近项目用到fastJson和jackson,为了决定到底弃用哪个,随手写了个测 ...

  8. Oracle 后台进程(六)PMON进程

    一.PMON简介 二.PMON的工作内容如下: 1.监控后台进程运行状况 2.如果某些进程异常中断,PMON去释放会话资源以及占用的锁LOCK 3.更新事务表的标志以及清除事务XID的标记 4.清除异 ...

  9. 049_指定目录路径,脚本自动将该目录使用 tar 命令打包备份到/data 目录

    #!/bin/bash[ ! -d /data ] && mkdir /data  #确保有此目录[ -z $1 ] && exit   #为空不需要打包if [ -d ...

  10. 1617:转圈游戏 ybt

    1617:转圈游戏 时间限制: 1000 ms         内存限制: 524288 KB提交数: 540     通过数: 326 [题目描述] nn 个小伙伴(编号从 00 到 n−1n−1 ...