Java 序列化与反序列化

概述

在日常开发过程中,有些场景下可能需要将应用程序中的对象信息保存在文件中,此时就需要将对象转换为流文件进行保存。这个转化的过程就叫 序列化,Java 本身也是支持序列化的,本篇博客我就来简单介绍下 Java 序列化相关的知识。


Java 序列化与反序列化

首先我们来了解下序列化与反序列化的概念:

  • 序列化:内存中保存的数据都是暂时的,如果某个对象需要长期保存,这时就需要将对象转化为流文件写入存储媒介中,这个转化的过程就是序列化

  • 反序列化:仅仅写入存储媒介肯定是不够的,当后面我们需要使用时还需要读取流文件并转化为对象,这个反向转化的过程就是反序列化。

Java 代码中通过 Serializable 接口实现对象的序列化:如果某个对象想要序列化,就必须实现该标记接口,实现该接口的对象就可以执行序列化和反序列化操作。


Serializable

Serializable 是 Java 提供的一个空接口,具体示例如下:

public class SerializableTest implements Serializable {

    private static final long serialVersionUID = -1364948644692894869L;

    private int id;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

从上图可以看出,实现 Serializable 接口不需要实现任何方法,只需要实现 serialVersionUID 属性。需要注意的一点是:serialVersionUID 属性是否实现是可选的。即使没有实现该属性,对象仍然可以进行序列化以及反序列化操作。

serialVersionUID:该属性主要用来确保序列化和反序列化的顺利执行。序列化后,流文件中 serialVersionUID 属性和类 serialVersionUID 属性相等时才能正常进行反序列化。具体过程是这样的:

  1. 序列化过程中,系统将类的 serialVersionUID 属性写入流文件保存到文件中
  2. 反序列化过程中,根据 serialVersionUID 判断文件是否当前类型,如果 serialVersionUID 属性相等,则可以进行反序列化操作,否则抛出异常。

当系统判断 serialVersionUID 属性不同导致反序列失败时,会抛出如下异常:

java.io.InvalidClassException: com.qhd.worker.SerializableTest; 
local class incompatible: stream classdesc serialVersionUID = -2776343930999941918, 
local class serialVersionUID = -2776343930999941950

这里我建议在使用序列化时实现 serialVersionUID 属性,这样即使序列化后的流文件收到轻微损坏,也不会影响反序列化的过程。如果不实现该属性的话,哪怕序列化后的流文件多一个空格,也可能造成 serialVersionUID 属性改变,反序列化操作时报。

需要特别注意的一点是:serialVersionUID 属性只是判断的一环,不是说 serialVersionUID 属性相等就一定能反序列化,如果对象属性都不相同的话,反序列化一定也会报错。


序列化、反序列化测试

下面我们通过具体测试用例看看序列化和反序列化的过程:

@Test
public void test() throws IOException, ClassNotFoundException {
    SerializableTest s = new SerializableTest();
    s.setId(1);
    s.setName("liming");

    // 序列化过程
    ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("D://1.txt"));
    oo.writeObject(s);
    oo.close();

    // 反序列化过程
    ObjectInputStream oi = new ObjectInputStream(new FileInputStream("D://1.txt"));
    SerializableTest ss = (SerializableTest) oi.readObject();
    oi.close();

    System.out.println(s);
    System.out.println(ss);
    System.out.println("判断是否相同对象:" + (s == ss ? "True" : "False"));
}

执行结果


SerializableTest{id=1, name='liming'}
SerializableTest{id=1, name='liming'}
判断是否相同对象:False

其中序列化之后,写入文件的流文件格式如下图所示:
序列化
从执行结果可以看出,反序列化后创建了一个全新的对象,并且该对象属性和序列化前对象完全相同,也就是说该对象所有属性都通过了序列化和反序列化的过程。

需要注意的一点是,不是所有类型的属性都可以进行序列化操作,以下两种类型的属性会跳过序列化过程:

  • 序列化只针对对象属性,static 修饰的类变量不会进行序列化
  • transient 修饰的对象属性也不会进行序列化操作

下面我们看一组新的测试案例:

public class SerializableTest implements Serializable {

    private static final long serialVersionUID = -1364948644692894869L;
    // 全局变量
    private static int staticParam;
    private int id;
    private String name;
    // transient 修饰成员变量
    private transient int transientParam;

	// 其他方法省略
	...
}

@Test
public void test2() throws IOException, ClassNotFoundException {
    SerializableTest s = new SerializableTest();
    s.setId(1);
    s.setName("liming");
    s.transientParam = 1;

    // 序列化过程
    ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("D://1.txt"));
    oo.writeObject(s);
    oo.close();

    // 反序列化过程
    ObjectInputStream oi = new ObjectInputStream(new FileInputStream("D://1.txt"));
    SerializableTest ss = (SerializableTest) oi.readObject();
    oi.close();

    System.out.println(s);
    System.out.println(ss);
}

执行结果

SerializableTest{transientParam=1, id=1, name='liming'}
SerializableTest{transientParam=0, id=1, name='liming'}

从输出结果可以看出,序列化前后 transientParam 属性值发生变化,也就是说该属性并没有参与到序列化的过程中去,因此反序列化新对象该属性默认设为0。

关于类属性很好理解,因为序列化和反序列化都是针对对象而言,而类属性是所有对象共享的内存模块,因此序列化前后不需要关注 static 类属性变量。


序列化、反序列化与引用属性

上面示例中的属性基本都是常量类型,下面我们来看一个引用类型的示例:

class Node implements Serializable {

    NodeParam nodeParam;

    public Node(NodeParam nodeParam) {
        this.nodeParam = nodeParam;
    }

    @Override
    public String toString() {
        return "Node{" +
                "nodeParam=" + nodeParam +
                '}';
    }
}

class NodeParam {
    int id;
    String name;

    public NodeParam(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "NodeParam{" +
                "id=" + id +
                ", name=" + name +
                '}';
    }
}

@Test
public void test3() throws IOException, ClassNotFoundException {
    NodeParam nodeParam = new NodeParam(1, "xiaoming");
    Node node = new Node(nodeParam);

    // 序列化过程
    ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("D://1.txt"));
    oo.writeObject(node);
    oo.close();

    // 反序列化过程
    ObjectInputStream oi = new ObjectInputStream(new FileInputStream("D://1.txt"));
    Node node2 = (Node) oi.readObject();
    oi.close();

    System.out.println(node);
    System.out.println(node2);
    System.out.println(node.nodeParam == node2.nodeParam);
}

执行结果

java.io.NotSerializableException: com.qhd.worker.SerializableTest$NodeParam

通过异常可以看出,nodeParam 属性不能序列化,因此程序报错。解决办法也很简单,只需要让 NodeParam 类实现 Serializable 接口,把该类转换为可序列化的即可:

class NodeParam implements Serializable {
	// 省略
}

执行结果

Node{nodeParam=NodeParam{id=1, name=xiaoming}}
Node{nodeParam=NodeParam{id=1, name=xiaoming}}
false

从执行结果可以看出,对于引用类型变量只需类实现 serializable 接口即可同时序列化。并且需要特别注意的一点是,序列化前后引用类型属性是不相同的,也就是说序列化创建的是对象本身,而不是引用。


重写序列化方法

在 Java 代码中,可以通过实现以下方法重写序列化过程,具体我们看代码:

class NodeA implements Serializable {

    private static final long serialVersionUID = -4767927628280154118L;

    private int id;
    private String name;

    public NodeA(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "NodeA{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    /**
     * 序列化时,系统首先会先调用 writeReplace方法
     * 通过该方法获取需要序列化的对象
     *
     * @return
     * @throws ObjectStreamException
     */
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("获取需要序列化的对象");
        return this;
    }

    /**
     * 读取到序列化对象后,调用 writeObject方法
     * 通过该方法序列化对象属性,这里我们只序列化 name 属性
     *
     * @param out
     * @throws IOException
     */
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        System.out.println("序列化对象属性");
        out.writeObject(this.name == null ? "qhd" : this.name);
    }

    /**
     * 反序列化时,系统首先会先调用 readObject方法
     * 通过该方法反序列化我们之前序列化的属相
     *
     * @param in
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(java.io.ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        System.out.println("反序列化属性");
        this.name = (String) in.readObject();
    }


    /**
     * 反序列化读取属性完毕后,调用 readResolve方法
     * 通过该方法将反序列化对象返回
     *
     * @return
     * @throws ObjectStreamException
     */
    private Object readResolve() throws ObjectStreamException {
        System.out.println("返回反序列化创建的新对象");
        return this;
    }
}

执行结果

获取需要序列化的对象
序列化对象属性
反序列化属性
返回反序列化创建的新对象
NodeA{id=1, name='liming'}
NodeA{id=0, name='liming'}

从执行结果可以看出,序列化过程中按次序调用了如上方法,并且只序列化了 name 属性。


序列化使用的场景

一般情况下,Java 序列化主要在以下几个场景中使用:

  • 内存中的对象写入内存
  • 使用套接字通过网络传输对象
  • 通过远程方法调用传输对象

不过通常情况下,通过数据库来保存对象数据。序列化主要用于网络传输对象过程中。


参考
https://blog.csdn.net/javazejian/article/details/52665164
  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值