详解Java的序列化和反序列化

标签: Java

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/09/ )。

1. 概述

序列化:把Java对象转换为字节序列的过程。

反序列化:把字节序列恢复为Java对象的过程。

对象的序列化主要有两种用途: 

2. 使用

要想实现序列化,必须要实现下面两个接口之一:

使用时,你只想隐藏一个属性,比如用户对象user的密码pwd,如果使用Externalizable,并除了pwd之外的每个属性都写在writeExternal()方法里,这样显得麻烦,可以使用Serializable接口,并在要隐藏的属性pwd前面加上transient就可以实现了。如果要定义很多的特殊处理,就可以使用Externalizable。

当然这里我们有一些疑惑,Serializable 中的writeObject()方法与readObject()方法科可以实现自定义序列化,而Externalizable 中的writeExternal()和readExternal() 方法也可以,他们有什么异同呢? * readExternal(),writeExternal()两个方法,这两个方法除了方法签名和readObject(),writeObject()两个方法的方法签名不同之外,其方法体完全一样。 * 需要指出的是,当使用Externalizable机制反序列化该对象时,程序会使用public的无参构造器创建实例,然后才执行readExternal()方法进行反序列化,因此实现Externalizable的序列化类必须提供public的无参构造。 * 虽然实现Externalizable接口能带来一定的性能提升,但由于实现ExternaLizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式来实现序列化。

  1. 序列化版本

在序列化过程中,可以控制序列化的版本。该字段为被序列化对象中的serialVersionUID字段。

public class User implements Serializable {  
private static final long serialVerisionUID = 1L ;  
...  
}  

一个对象数据,在反序列化过程中,如果序列化串中的serialVersionUID与当前对象值不同,则反序列化失败,否则成功。

如果serialVersionUID没有显式生成,系统就会自动生成一个。生成的输入有:类名、类及其属性修饰符、接口及接口顺序、属性、静态初始化、构造器。任何一项的改变都会导致serialVersionUID变化。

属性的变化都会导致自动生成的serialVersionUID发生变化。例如,对于对象A,我们生成序列化的S(A),然后修改A的属性,则此时A的serialVersionUID发生变化。反序列化时,S(A)与A的serialVersionUID不同,无法反序列化。会报序列号版本不一致的错误。

为了避免这种问题, 一般系统都会要求实现serialiable接口的类显式的生明一个serialVersionUID。显式定义serialVersionUID的两种用途:  * 希望类的不同版本对序列化兼容时,需要确保类的不同版本具有相同的serialVersionUID;  * 不希望类的不同版本对序列化兼容时,需要确保类的不同版本具有不同的serialVersionUID。

如果我们保持了serialVersionUID的一致,则在反序列化时,对于新增的字段会填入默认值null(int的默认值0),对于减少的字段则直接忽略。

  1. 使用举例

我们假设有一个对象需要进行序列化:

public class User implements Serializable {
    
    private static final long serialVersionUID = 25454531454L;
    
    private String name;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
}

然后,可以通过以下方式进行序列化和反序列化:

public class Main {
 
    public static void main(String[] args) {
        private static String FILE_NAME = "D:/userData.bin";
        
        //序列化
        User user = new User();
        user.setName("xiaoming");
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME));
            oos.writeObject(user);
            oos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        //反序列化
        User newUser = null;
        try {
            ObjectInput input = new ObjectInputStream(new FileInputStream(FILE_NAME));
            newUser = (User)input.readObject();
            input.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("name = " + newUser.getName());
    }
 
}

这样,我们将对象user的信息序列化后写入到二进制文件中,并读出来之后又反序列化为newUser对象。

关于序列化的使用,推荐大家阅读MyBatis的源码。为什么呢?因为MyBaits作为一个ORM框架,需要频繁进行内存中对象和数据库中序列化字符串的转换。在这方面,用的很到位。

这方面,推荐一本书《通用源码阅读指导书》。这是一本以MyBatis的源码为实例讲述源码阅读方法的书籍,非常推荐。

通用源码阅读指导书-京东自营

《通用源码阅读指导书》

在书中的第5章介绍了序列化的基本知识和异常信息的序列化、第19章介绍了缓存中的序列化装饰器、第22章介绍了序列化中的钩子方法和MyBatis懒加载功能中的序列化。非常棒!

多读读源码,进步会很大。

本文首发于个人知乎:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐