进阶我爱编程

序列化详解

2018-04-11  本文已影响204人  Hypercube

1.序列化

序列化是指将对象按照某种协议格式转化为二进制字节序列;对应的,反序列化将二进制字节序列恢复为对象。
序列化常用于两种情况:

  1. 数据持久化。利用序列化将对象持久化到外部存储中,然后在合适的时机,通过反序列化恢复为对象。目前此种用法较少,使用数据库持久化是更好的选择。
  2. 网络传输对象。将对象序列化为二进制字节数据,通过网络传输到远程机器,然后通过反序列化恢复为对象。这是序列化使用的主要场景,远程方法调用RMI和远程过程调用RPC都依赖于序列化。

JAVA序列化的设计初衷是为了保存对象的状态从而保存进程状态,以便在合适的时机恢复进程,但目前已基本脱离这种用法。JAVA序列化的另一大用法是远程过程调用RMI,由于性能不高(序列化后所占存储空间过大且序列化耗时较高)的原因也很少用于实际生产环境中。生产环境中,建议使用性能更高的序列化方案,比如Google的ProtoBuf、FaceBook的Thrift,以及用于HTTP的JSONXML等等。尽管原生JAVA序列化方案已较少使用,但理解其原理也大有裨益。

2. 序列化的基本用法

在JAVA中,一个对象要实现序列化是极其简单的:实现Serializable接口即可。

class User implements Serializable {
    private String name;
    private String password;
    
    public User(String name, String password) {
        this.name = name;
        this.password = password;
        System.out.println("constructor");
    }
}

然后再配合ObjectOutputStreamObjectInputStream即可实现对象的序列化和反序列化,一个简单的使用示例如下:

public static void main(String[] args) throws Exception {
    User user = new User("root", "root");

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    ObjectOutputStream out = new ObjectOutputStream(outputStream);
    out.writeObject(user);
    out.close();

    ObjectInputStream in = new ObjectInputStream(
            new ByteArrayInputStream(outputStream.toByteArray()));
    User user1 = (User) in.readObject();
    System.out.println(user1);
}

在示例中,实现了将User对象序列化到ByteArrayOutputStream中的字节数组中,然后使用反序列化将字节数组恢复为一个新的对象。一般的,也可以将对象序列化到FileOutputStream中实现持久化。
再次回顾示例代码,需要注意以下几点:

2.1 Serializable是一个标记接口

Serializable只是一个标记接口,其中没有任何方法。但如果一个待序列化对象不实现该接口,将会抛出NotSerializableException异常。其原理,可参考JDK的ObjectOutputStream源码

    if (obj instanceof String) {
        writeString((String) obj, unshared);
    } else if (cl.isArray()) {
        writeArray(obj, desc, unshared);
    } else if (obj instanceof Enum) {
        writeEnum((Enum) obj, desc, unshared);
    } else if (obj instanceof Serializable) {
        writeOrdinaryObject(obj, desc, unshared);
    } else {
        throw new NotSerializableException(cl.getName());
    }

由此,可知除了实现Serializable接口的对象外,StringArrayEnum也都可以序列化。另外,代码中没有列出的JAVA基本数据类型:intchardouble等也默认可序列化。

2.2 可使用serialVersionUID进行版本控制

如果跨进程使用序列化机制,一个基本要求是:两个进程含有相同的对象class文件,相同指对象的功能和类路径都必须一致。除此之外,两个类的序列化ID也必须一致。JAVA序列化机制使用一个如下的变量serialVersionUID表示序列化ID:

    ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

如果两个进程中该值不一致,反序列化时会抛出InvalidClassException异常,从而提醒用户该对象的类可能已被修改了某些字段,需要更新class文件。
注意最开始的示例中没有指定这个变量,此时,编译器会根据类名、接口以及其中所含字段自动生成一个默认值。这个默认值随编译器的实现细节不同而不同,所以强烈建议显式指明该变量值。其中访问权限修饰符建议使用private,一个可序列化对象完整的示例如下:

class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String password;
}

显式声明该变量值的另一个好处是:可以利用序列化的兼容机制,即:

  1. 新增一个字段。
  2. 改变字段的访问权限修饰符。publicprotectedpackageprivate不会影响序列化。
  3. 将一个字段从static修改为非static或者transient修改为非transient。

这几种情况下,反序列化的class不必和序列化时的class代码完全一致。但如果使用默认serialVersionUID则会生成不一致的值,从而抛出InvalidClassException异常。

2.3 子类的序列化

当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。当一个子类实现序列化,而父类没有实现序列化,情况又会怎么样呢?

class User {
    private String name;
    private String password;
    
    // 构造方法省略
}

class Vip extends User implements Serializable{
    private int money;

    public Vip(String name, String password, int money) {
        super(name, password);
        this.money = money;
    }
}

序列化正常,但反序列化时抛出如下异常:

Exception in thread "main" java.io.InvalidClassException: serial.Vip; no valid constructor

为了实现反序列化,有两种方法:

  1. 父类也实现Serializable接口,此时完美符合序列化机制要求。
  2. 父类不实现Serializable接口,但提供一个无参构造方法。此时,子类中的字段可以恢复为序列化前的值,但父类中的字段为初始化默认值。

方法2的示例输出如下:

    Vip{money=5,name=null,password=null}

2.4 静态变量的序列化

JAVA序列化保存的是对象的状态,而静态变量不是对象的状态而是类的状态,所以序列化不保存静态变量

2.5 反序列化不会调用构造方法

JAVA在反序列化时不会调用构造方法。当类有继承关系时,情况又稍有不同。正如2.3的示例,如果子类实现Serializable接口而父类并未实现,但父类有无参构造方法时,尽管子类的构造方法不会被调用,但父类的无参构造方法将会被调用。给UserVIP提供如下的构造方法:

    public User() {
        System.out.println("user no-arg constructor");
    }
    
    public Vip(String name, String password, int money) {
        super(name, password);
        this.money = money;
        System.out.println("vip constructor");
    }

则2.3示例反序列化的输出为:

    user no-arg constructor
    Vip{money=5,name=null,password=null}

可见,子类的构造方法不会被调用,但父类的无参构造方法被调用。

3.自定义序列化

3.1 transient

特殊情况下,某些字段并不希望被序列化,比如说User类中的密码password字段,并不希望被序列化到文件中。也许你会联想到前述子类序列化的示例,父类的字段不会被序列化,可以实现该需求。但这不是好的方式,JAVA提供了一种更好的方法:使用transient关键字。示例如下:

class User implements Serializable{
    private static final long serialVersionUID = 42L;

    private String name;
    private transient String password;
    
    // 构造方法和getter setter省略
}

在不希望被序列化的字段前添加transient关键字,序列化时将忽略该字段。示例的输出如下:

    User{name='root', password='null'}

3.2 writeObject和readObject

在上述的密码password字段的处理中,加密该字段然后序列化到文件是一种更好的方法。可通过在待序列化类添加如下的方法实现:

class User implements Serializable {
    private static final long serialVersionUID = 42L;

    private String name;
    private transient String password;
    
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeUTF(encrypt(password));
    }
    
    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        String encryptStr = in.readUTF();
        System.out.println("encrypt=" + encryptStr);
        this.password = decrypt(encryptStr);
    }
}

注意其中的writeObject()readObject()方法,可用这两个方法对序列化和反序列化进行特殊处理。本例中,序列化时对密码加密,反序列化时进行解密。本例的输出如下:

    encrypt=63A9F0EA7BB98050796B649E85481845
    User{name='root', password='root'}

如果要实现自己的特殊处理,注意这两个方法的方法签名必须和示例一致,其中访问权限修饰符注意为private,JAVA是通过反射调用的这两个方法。此外,defaultWriteObject()defaultReadObject()是调用JAVA序列化的默认实现。

3.3 Externalizable

第三种自定义序列化过程的方法是:实现Externalizable接口,该接口是Serializable的子接口,代码如下:

public interface Externalizable extends java.io.Serializable {
    // 序列化
    void writeExternal(ObjectOutput out) throws IOException;
    // 反序列化
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

如果序列化时实现该接口,则需要自主控制所有序列化细节。也就是说,该接口是一个正常的接口,使用时遵循JAVA语法即可。作为标记接口的Serializable反序列化时不会调用构造方法,而实现该接口反序列时则需要调用类的无参构造方法,然后调用readExternal()方法。使用该接口序列化的示例如下:

class User implements Externalizable {
    private static final long serialVersionUID = 42L;

    private String name;
    private transient String password;

    public User() {}
    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeUTF(encrypt(password));
    }
    
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = in.readUTF();
        String encryptStr = in.readUTF();
        System.out.println(encryptStr);
        this.password = decrypt(encryptStr);
    }
}

最后附上JAVA序列化相关的资料:

  1. Java Object Serialization Specification
  2. Java 对象序列化
  3. Java 序列化的高级认识
上一篇下一篇

猜你喜欢

热点阅读