一步步分析Java深拷贝的两种方式-clone和序列化

今天遇到一道面试题,询问深拷贝的两种方法。主要就是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异常

  2. 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异常。

  2. 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接口。

原文地址:https://www.cnblogs.com/zaid/p/12043834.html

时间: 2024-10-10 06:10:29

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

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

1 public class SellTicketDemo { 2 public static void main(String[] args) { 3 // 创建三个线程对象 4 SellTicket st1 = new SellTicket(); 5 SellTicket st2 = new SellTicket(); 6 SellTicket st3 = new SellTicket(); 7 8 // 给线程对象起名字 9 st1.setName("窗口1"); 10 st2.

org.apache.hadoop.yarn.conf.ConfigurationProviderFactory分析加载配置文件两种方式

ConfigurationProviderFactory结构如下: /** * Creates an instance of {@link ConfigurationProvider} using given * configuration. * @param bootstrapConf * @return configurationProvider */ @SuppressWarnings("unchecked") public static ConfigurationProvide

采用java和数据库两种方式进行加锁

java代码加锁 publicstatic synchronized int generate(StringtableName){Stringsql = "select value from t_table_id where table_name=?";Connectionconn = null;PreparedStatementpstmt = null;ResultSetrs = null;intvalue = 0;try{conn= DbUtil.getConnection();p

Java Thread Synchroize两种方式的优劣

Context: 关于Thread老是看了忘,看了忘记.还是写下来吧.翻译的是orcale官网的锁解释文档地址 锁: 同步机制是围绕内部实体即intrinsic lock(intrinsic固有的.本质的).monitor lock(监控锁).Intrinsic lock在同步上扮演两个角色:1.强制的排他性读取对象的状态(也就是线程访问对象的属性.方法等是互斥的).2保持happens-before relationship来确保变量.方法的可见性. 每个对象都有与其关联的intrinsic

java生成Excel两种方式

方法一,利用第三方jar包:jxl.jar 无论方法一方法二最后最好用转换成流传给前端. 1 public void createExcel(){ 2 try{ 3 //打开文件 4 WritableWorkbook workbook = Workbook.createWorkbook(new File("test.xls")); 5 //生成名为"第一页"的工作表,参数0表示这是第一页 6 WritableSheet sheet = workbook.create

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

java方式: publicstatic synchronized int generate(StringtableName){ Stringsql = "select value from t_table_id where table_name=?"; Connectionconn = null; PreparedStatementpstmt = null; ResultSetrs = null; intvalue = 0; try{ conn= DbUtil.getConnecti

JAVA连接Oracle两种方式

以前学习.NET连接数据库时,也是做各种的连接配置.而如今在用到java操纵Oracle时,同样是一些的操作.在此总结一下. 1.JDBC_ODBC连接数据库 此方法需要配置ODBC数据源,本机必须有Oracle数据库 1.配置数据源 打开控制面板--管理工具--数据源 2.接下来在代码中连接即可 //使用jdbc_odbc桥接方式连接,需要配置数据源 public static void main(String [] args){ try{ //1.加载驱动 Class.forName("su

java及javascript两种方式获取YYYY-MM-dd HH:mm:ss格式的日期字符串

一.java方式 String leaveTime=""; SimpleDateFormat sDateFormat=new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"); Date date=new Date(); leaveTime=sDateFormat.format(date); 二.javascript方式 <script type="text/javascript"> $().ready(

java 创建对象的两种方式

一: Object object = new Object(); 二: public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException { //可以通过映射的方式来获取 DemoTest2 myobject = (DemoTest2)Class.forName("DemoTest2").newInstance(); m