Java基础——Serializable/Externalizable序列化接口

1,157 阅读5分钟

前言

Serializable序列化是将对象转成易于持久化、传输的格式的一种手段,通过将一个对象继承Serializable接口声明该类是可序列化的,然后通过ObjectOutputStream、ObjectInputStream流实现对象的存储与读取(在Serializable接口的源码注释中有这两个stream相关的简单的介绍)。

下面记录下几个序列化的例子。在演示例子之前,声明一个类:

public class User implements Serializable {
    // 构造函数省略
    private long id;
    private String userName;
    // getter、setter省略
}

Serializable序列化

public static void main(String[] args) {
    List<User> userList = new ArrayList<User>();
    userList.add(new User(1, "张三"));
    userList.add(new User(2, "李四"));
    try {
        FileOutputStream fileOutputStream = new FileOutputStream("UserSerializeFile");
        ObjectOutputStream objOutputStream = new ObjectOutputStream(fileOutputStream);
        objOutputStream.writeObject(userList);
        objOutputStream.close();
        fileOutputStream.close();
        System.out.println("序列化完成");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

执行完成后可在项目根目录看到文件UserSerializeFile。因为是以二进制的形式进行保存的,所以以普通文本的形式打开会显示乱码的形式。

Serializable反序列化

public static void main(String[] args) {
    try {
        FileInputStream fileInputStream = new FileInputStream("UserSerializeFile");
        ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
        List<User> list = (List<User>) inputStream.readObject();
        for (User user : list) {
            System.out.println("id: " + user.getId() + " userName: " + user.getUserName());
        }
        inputStream.close();
        fileInputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

执行完成后,可以看到以下结果:

代码比较简单,所以就不做其他介绍,此文主要是想记录一些相关的知识点。

1、不使用Serializable接口,会发生什么错误?

2、serialVersionUID的作用

在类的编辑页面,IDE提示"The serializable class User does not declare a static final serialVersionUID field of type long",其中提到的serialVersionUID具有什么作用?

根据Serializable接口的注释,可以得知:

序列化运行时与每个可序列化的类关联一个版本号,称为serialVersionUID。在反序列化过程中使用该版本号来验证序列化对象的发送者和接收者是否已加载了该对象的与序列化兼容的类。如果接收者已为该对象加载了一个与相应发送者类具有不同的serialVersionUID的类,则反序列化将导致InvalidClassException错误。可序列化的类可以使用一个static、final的字段来显式声明自己的serialVersionUID。
如果可序列化的类未明确声明serialVersionUID,则序列化运行时将根据该类的各个方面为该类计算默认的serialVersionUID值。但是,强烈建议所有可序列化的类显式声明serialVersionUID值,因为默认的serialVersionUID计算对类详细信息高度敏感,而类详细信息可能会根据编译器的实现而有所不同,因此在反序列化过程中可能导致InvalidClassException。此外还建议声明serialVersionUID时使用private修饰符,因为serialVersionUID字段作为继承成员不起作用。

请原谅我的工地英语,借助谷歌翻译勉强读懂了相关的介绍。既然看懂了,就需要亲自验证一下,以加深记忆。再上述代码以生成持久化文件的基础上,我对可序列化的类做了修改,新增了字段phone:

public class User implements Serializable {
    //构造函数省略
    private long id;
    private String userName;
    private String phone;
    //getter、setter省略
}

再次执行反序列化,得到以下结果: 

可见,在不显示声明serialVersionUID的情况下,一旦修改了序列化对象,之前保存的文件将无法再进行读取和解析。根据错误信息,我们可以知道持久化文件对应的serialVersionUID,于是我们为持久化对象增加serialVersionUID:

public class User implements Serializable {
    // 构造函数省略
    private static final long serialVersionUID = 4164300595038901719L;
    private long id;
    private String userName;
    private String phone;
    // getter、setter省略
}

再次执行反序列化,此次程序允许则没有再出现错误。

3、部分字段不想序列化,怎么办?

声明字段时使用transient关键字。无论是序列化还是反序列化,只有添加了transient,序列化运行时会跳过该字段的处理。

4、要进行序列化的类里的引用,是否需要可序列化?

需要。要进行序列化的类里如果有引用对象,该对象也需要声明为可序列,即便该对象只含有基本类型的属性,否则会报错误NotSerializableException。

5、关于序列化多次的问题

多个对象依次序列化与一个对象修改属性后重复序列化

public static void main(String[] args) throws IOException, ClassNotFoundException {
    //序列化
    User user1 = new User(1, "张三");
    User user2 = new User(2, "李四");
    FileOutputStream fileOutputStream = new FileOutputStream("UserSerializeFile");
    ObjectOutputStream objOutputStream = new ObjectOutputStream(fileOutputStream);
    objOutputStream.writeObject(user1);
    objOutputStream.writeObject(user2);
    objOutputStream.close();
    fileOutputStream.close();
    System.out.println("序列化完成");
    
    //反序列化
    FileInputStream fileInputStream = new FileInputStream("UserSerializeFile");
    ObjectInputStream inputStream = new ObjectInputStrea(fileInputStream);
    User user3 = (User)inputStream.readObject();
    User user4 = (User)inputStream.readObject();
    System.out.println("id: " + user3.getId() + " userName: " + user3.getUserName());
    System.out.println("id: " + user4.getId() + " userName: " + user4.getUserName());
    inputStream.close();
    fileInputStream.close();
}

结果为:

可见,对于不同对象的序列化,可以按顺序反序列化取出。
但如果是一个对象修改属性后序列化,结果则是最初的对象属性:

public static void main(String[] args) throws IOException, ClassNotFoundException {
    //序列化
    User user1 = new User(1, "张三");
    FileOutputStream fileOutputStream = new FileOutputStream("UserSerializeFile");
    ObjectOutputStream objOutputStream = new ObjectOutputStream(fileOutputStream);
    objOutputStream.writeObject(user1);
    user1.setUserName("李四");
    objOutputStream.writeObject(user1);
    objOutputStream.close();
    fileOutputStream.close();
    System.out.println("序列化完成");
    
    //反序列化
    FileInputStream fileInputStream = new FileInputStream("UserSerializeFile");
    ObjectInputStream inputStream = new ObjectInputStream(fileInputStream);
    User user3 = (User)inputStream.readObject();
    User user4 = (User)inputStream.readObject();
    System.out.println("id: " + user3.getId() + " userName: " + user3.getUserName());
    System.out.println("id: " + user4.getId() + " userName: " + user4.getUserName());
    inputStream.close();
    fileInputStream.close();
}


根据网上查询到的资料,说是java序列化算法不会重复序列化一个对象,在序列化一次之后,之后的序列化不会修改内容,只会保存序列化对象的编号,然而看了半天源码没找到实现该逻辑的地方,待日后了解了相关的知识再回来挖坟吧。

6、readObjectNoData、readResolve、writeReplace

在Serializable中还介绍了其他接口方法,通过重写可以实现特定场景的业务逻辑,如readObjectNoData可以实现基类空对象的填充,readResolve、writeReplace可以自定义读写,目前因为未遇到使用这些方法的应用场景,暂时先留个印象,以后遇到了再继续完善。

Externalizable序列化接口

与 Serializable相比,Externalizable是使用更加自由、性能更好的序列化接口,但相对的,需要开发人员自己一一指出需要序列化的对象,指定序列化的顺序。下面记录下使用Externalizable的基本方法:

public class User implements Externalizable {

    public User() {
    }

    public User(long id, String userName) {
        this.id = id;
        this.userName = userName;
    }

    private long id;
    private String userName;

    // gettter、setter省略

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeLong(id);
        out.writeObject(userName);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.id = in.readLong();
        this.userName = (String) in.readObject();
    }
}

需要说明的事,Externalizable序列化类需要添加无参构造函数,否则会报错: