【Java】对象的序列化和克隆详解

前言

在学习源码的过程中,常常看到很多类都实现了Cloneable接口或是Serializable接口,如集合类。虽然知道他们的作用是能进行对象序列化或者克隆,但是具体的功能却还是一知半解,所以花了些时间去系统地了解了一下他们。

正文

克隆也经常被称为拷贝(copy),比如很多面试官都会问深拷贝和浅拷贝,就是深克隆和浅克隆。

序列化和克隆

序列化 - Serializable

定义:将实现了Serializable接口(标记型接口)的对象转换成一个字节数组,并可以将该字节数组转为原来的对象。

简介

为什么我们需要序列化机制?

我们可以在JVM运行时随时使用我们的对象,因为他们都存放在JVM的堆内存里。但是一旦JVM停止运行,那么这些对象就全部没有了。

有很多情况,我们可能需要将对象持久化,然后在之后的某个时间,或者在另一台JVM上将其重新读取出来。我们需要一种通用的转换对象、读取对象的方式,于是Java的序列化应运而生。

对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。

在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。

ObjectOutputStream 是专门用来输出对象的输出流;
ObjectOutputStream 将 Java 对象写入 OutputStream。然后可以使用 ObjectInputStream 读取(重构)对象。

实例

假设我们有一个Person类,现在我们要写一个序列化方法把Person类序列化到一个txt文件中,然后写一个反序列化方法,将Person对象从txt文件中读取。

Person类

public class Person implements Serializable {

    private static final long serialVersionUID = -5483049468519854281L;  //手动指定UID

    String name;
    String sex;
    int age;

    public Person(String name, String sex, int age) {
        this.name = name;
        this.sex = sex;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }

}

要进行序列化,该类必须要实现Serializable接口。

方法类

public class SerializeTest {

	//序列化方法,将person对象序列化到person.txt文件
    public static void serializePerson() throws IOException {
        Person person = new Person("jack", "男", 20);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("person.txt")));
        oos.writeObject(person);
        System.out.println("序列化person成功.");
    }

	//反序列化方法,将person对象从person.txt文件中读取
    public static Person deserializePerson() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("person.txt")));
        Person person = (Person) ois.readObject();
        return person;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        serializePerson();
        Person person = deserializePerson();
        System.out.println(person.toString());
    }
}

输出结果:
在这里插入图片描述
说明序列化是成功的。

关于serialVersionUID

这里还要说一下为什么Person类中要设置serialVersionUID

Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)

1)如果没有添加serialVersionUID,进行了序列化,而在反序列化的时候,修改了类的结构(添加或删除成员变量,修改成员变量的命名),此时会报错。
2)如果添加serialVersionUID,进行了序列化,而在反序列化的时候,修改了类的结构(添加或删除成员变量,修改成员变量的命名),那么可能会恢复部分数据,或者恢复不了数据。

如果设置了serialVersionUID并且一致,那么可能会反序列化部分数据;如果没有设置,那么只要属性不同,那么无法反序列化。


克隆 - Cloneable

在实际编程过程中,会需要创建值与已经存在的对象A一样,但是却与A完全独立的一个对象,即对两个对象做修改互不影响,这时需要用克隆来创建对象B。

使用注意事项

要实现克隆,我们需要让将要克隆对象的类实现Cloneable接口,然后重写clone()方法。

需要注意的是,克隆对象的clone()方法并不属于Cloneable接口,它是Object类的一个方法。这一方法的定义如下:

protected native Object clone() throws CloneNotSupportedException;

可以看到他是一个protected方法,即Object的子类才可以使用此方法。如果在没有重写Object的clone()方法且没有实现Cloneable接口的实例上调用clone方法,会报CloneNotSupportedException异常。
为什么一定要实现一个空的Cloneable接口呢?原因在于它相当于一个标志,没有实现该接口的类是无法调用clone()方法的。这里的标志判断是在native方法中进行。

我们在重写clone()方法后,需要将该方法设置为public

这里简单介绍一下设置为public的原因。我们知道,protected修饰的方法只允许本类、或本包的类、或它的子类使用。也就是说,Object的clone()方法可以被任何对象使用,因为他是任何类的父类。如果我们在自己定义的类A中重写的clone()方法仍然是protected的,那么在另一个不在A所在包的类B中就无法调用A的clone(),也就无法克隆A对象了。

我们也可以通过new一个对象,然后对其各个属性赋值,也能实现该需求,但是相比clone方法要做的工作太多,所以不推荐。

深克隆和浅克隆
浅克隆

Object类中的clone()方法产生的效果是:在当前内存中开辟一块和原始对象一样的内存空间,然后原样拷贝原始对象中的内容。对基本数据类型来说,这样的操作不会有问题,但是对于非基本类型的变量(如自定义的类),保存的仅仅是对象的引用,导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象,对非基本类型的变量的操作会相互影响。

举个栗子。

假如我们有2个类,一个是Info类,记载一个同学的信息,一个是Person类,它包括了一个Info类记载信息,同时还有一个id值。这时如果我们要克隆一个Person对象,那么浅拷贝仅仅是保证id值相等,里面的的Info引用还是指向同一个Info对象的,此时如果我们要修改新Person的Info,原来Person的Info也会受到影响。

下面来测试一下。

Info类

/**
 * Created by makersy on 2019
 */

public class Info {

    String name;
    int num;


    public Info(String name, int num) {
        this.name = name;
        this.num = num;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Info info = (Info) o;
        return num == info.num &&
                Objects.equals(name, info.name);
    }

}

重写了equals方法方便比较两个Info对象的值是否相等。

Person类

/**
 * Created by makersy on 2019
 */

public class Person implements Cloneable {

    int id;
    Info info;

    public Person() {
    }

    public Person(int id, Info info) {
        this.id = id;
        this.info = info;
    }

	//重写clone()方法
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id &&
                Objects.equals(info, person.info);
    }
    

    public static void main(String[] args) throws CloneNotSupportedException {
        Info info = new Info("name", 1);

        Person p = new Person(1, info);  //p -> 原对象
        Person pc = (Person) p.clone();  //pc -> 克隆后的对象

        System.out.println( p==pc );                   //false
        System.out.println( p.equals(pc));             //true
        System.out.println( p.info.equals(pc.info) );  //true
        System.out.println(p.info == pc.info);         //true

    }
}

从结果我们知道,pc和p是不同的两个对象,内存地址也不相同;并且pc和p内部的值是一样的。但是info却是内存地址相同的同一个对象,这就不符合我们对“两对象操作互不干扰”的期望。

结论:

1、克隆一个对象不会调用对象的构造方法。
2、浅克隆对对象基本数据类型的修改不会互相影响,对对象非基本数据类型的修改会相互影响,所以需要实现深克隆。

深克隆

深克隆在clone()方法中除了要克隆自身对象,还要对其非基本数据类型的成员变量克隆一遍,所以这就导致非基本数据类型的成员变量也需要实现上述的实现clone的两个操作。

深克隆的方式有两种:

  1. 使用Cloneable接口
    第一步,克隆的类要实现Cloneable接口和重写Object的clone()方法;
    第二步,先调用super.clone()方法克隆出一个新对象,然后手动给克隆出来的对象的非基本数据类型的成员变量赋值。

  2. 使用序列化
    克隆对象实现Serializable接口。先对对象进行序列化,紧接着马上反序列化。需要注意克隆对象的非基本数据类型成员也需要实现Serializable接口,否则会报错,成员无法被序列化。

在数据结构比较复杂的情况下,序列化和反序列化可能实现起来简单,方法1实现比较复杂。但是方法1效率会高一些,因为clone方法是native方法嘛。

下面介绍下这两种方法。示范类还是采用上述的两个Info和Person。


第一种方法

Info类

/**
 * Created by makersy on 2019
 */

public class Info implements Cloneable{

    String name;
    int num;


    public Info(String name, int num) {
        this.name = name;
        this.num = num;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Info info = (Info) o;
        return num == info.num &&
                Objects.equals(name, info.name);
    }
}

Info没什么大变化,只是实现了Cloneable接口,覆盖了clone()方法。

Person类

/**
 * Created by makersy on 2019
 */

public class Person implements Cloneable {

    int id;
    Info info;

    public Person(int id, Info info) {
        this.id = id;
        this.info = info;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        Person person = (Person) super.clone();
        Info temp = (Info) person.info.clone();
        person.info = temp;
        return person;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id &&
                Objects.equals(info, person.info);
    }


    public static void main(String[] args) throws CloneNotSupportedException {
        Info info = new Info("name", 1);


        Person p = new Person(1, info);

        Person pc = (Person) p.clone();

        System.out.println( p==pc );                   //false
        System.out.println( p.equals(pc));             //true
        System.out.println(p.info == pc.info);         //false
        System.out.println( p.info.equals(pc.info) );  //true

        pc.info.name = "张三";
        System.out.println(p.info.name);               //name
        System.out.println(pc.info.name);              //张三

    }
}

实现深克隆之后,对新对象pc的info进行更改,并不会影响到p的info。这就证明两个Person对象的基本数据类型和非基本数据类型都实现了克隆。


第二种方法:序列化
Info类

/**
 * Created by makersy on 2019
 */

public class Info implements Serializable {

    private static final long serialVersionUID = 2316822637750102608L;  //最好显式声明序列化ID

    String name;
    int num;

    public Info(String name, int num) {
        this.name = name;
        this.num = num;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Info info = (Info) o;
        return num == info.num &&
                Objects.equals(name, info.name);
    }

}

Person类


/**
 * Created by makersy on 2019
 */

public class Person implements Serializable {

    private static final long serialVersionUID = 4773247170650162945L;

    int id;
    Info info;

    public Person() {
    }

    public Person(int id, Info info) {
        this.id = id;
        this.info = info;
    }

    //序列化实现深克隆,返回深克隆对象
    public Person myClone() {
        Person person = null;
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);  //序列化对象
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            person = (Person) ois.readObject();  //反序列化对象
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return person;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id &&
                Objects.equals(info, person.info);
    }


    public static void main(String[] args) throws CloneNotSupportedException {
        Info info = new Info("name", 1);


        Person p = new Person(1, info);

//        Person pc = (Person) p.clone();
        Person pc = p.myClone();

        System.out.println( p==pc );
        System.out.println( p.equals(pc));
        System.out.println(p.info == pc.info);
        System.out.println( p.info.equals(pc.info) );

        pc.info.name = "张三";
        System.out.println(p.info.name);
        System.out.println(pc.info.name);

    }
}

没有贴输出结果,因为和第一种方式结果是一样的。

总结

本篇文章记录了为什么要序列化机制,如何将对象序列化和反序列化,如何对对象进行深、浅克隆,以及在使用这些方式时的一些注意事项。
关于这些,还有更深层次的东西等待我去挖掘,我要继续努力呀~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值