Java之Serializable/Externalizable序列化和反序列化

Java之Serializable/Externalizable序列化和反序列化

文章链接:

知识点:

  1. 序列化和反序列化介绍;
  2. 为什么需要序列化和反序列化;
  3. Serializable接口序列化和反序列化;
  4. Externalizable接口序列化和反序列化;
  5. 兼容性问题;
  6. 序列化和反序列化得到的对象问题;
  7. 新名词记录{Serializable;Externalizable;序列化版本兼容问题-serialVersionUID;}

概述

对象刚入门编程的人都知道,因为在Java中,任何事物都是对象。

我们在编程时,想要什么对象,new一个出来使用就OK了。这是因为Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即这些对象的生命周期不会比JVM的生命周期更长。随着JVM被shutdown,这些new出来的对象便会随之被释放而消失。

但是在现实中,有可能需要保存一份用户浏览过的一组数据以便用户打开应用直接查看,或者是将这组数据在有网之后提交到后台去。

那么如果我要在JVM停止之后,还想得到我需要的数据对象呢?但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。

关于Android中activity数据持久化的文章请看

这就要使用到序列化和反序列化了。

序列化和反序列化:*Java的对象序列化是指将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。对象的序列化是基于字节,不能使用io流中以字符读取操作的Reader和Writer。*

对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。反之亦然。支持序列化和反序列化的基本类型有:String,Array,Enum和Serializable,如果非以上的一种,那么会抛出NotSerializableException异常。

因为枚举类默认继承java.lang.Enum类,而此类又是实现了Serializable接口,所以枚举类型对象都是可以被序列化的。

对于序列化和饭序列化要使用到的io操作类主要有:FileOutputStream,ByteArrayOutputStream,ObjectOutputStream,FileInputstream,ByteArrayInputStream,ObjectInputStream等等,而我们要调用的是writeObject()方法和readObject()方法。

为什么要序列化:这一过程甚至可通过网络进行,这意味着序列化机制能自动弥补不同操作系统之间的差异。

Serializable序列化和反序列化

下面是具体的实例操作:
首先需要建立一个实体类,创建UserBean.java类,实现Serializable接口。

package com.yaojt.sdk.java.bean;

import java.io.Serializable;
public class UserBean implements Serializable {

    //串行化版本统一标识符
    private static final long serialVersionUID = 1L;

    private String userName;
    private String password;
    private int age;

    public UserBean(String userName, String password, int age) {
        this.userName = userName;
        this.password = password;
        this.age = age;
    }

    //一系列的setter和getter方法,省略了
}

然后对此类进行序列化和反序列化操作:

public void serializableTest() {
        UserBean userBean = new UserBean("yaojt", "123456", 25);
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(userBean);
            objectOutputStream.flush();
            objectOutputStream.close();
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            UserBean userBean1 = (UserBean) objectInputStream.readObject();
            CommonLog.logInfo("Serializable,objectOutput反序列化", userBean1.toString());
            //结果:Serializable,objectOutput反序列化: :{userName:yaojt, password:123456}
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

以上的方法,是利用文件流进行的序列化和反序列化操作。

这里始终要记得:当io流不需要使用到时,一定要进行关闭流操作,否则很可能引起内存泄漏。我这里只是简单的在try{}catch{}里头进行io流的关闭(为了简洁明了),比较正确的做法是要在finally代码块里头进行关闭操作。

下面将使用字节数组流进行序列化和反序列化的操作。

public void writeSerializableByArrayTest() {
        ByteArrayOutputStream byteArrayOutputStream = null;
        try {
            byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            UserBean userBean = new UserBean("tanksu", "123456", 25);
            objectOutputStream.writeObject(userBean);
            objectOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            UserBean userBean = (UserBean) objectInputStream.readObject();
            CommonLog.logInfo("serializable,byteArray反序列化", userBean.toString());
            //serializable,byteArray反序列化: :{userName:tanksu, password:123456}
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

通过以上几个操作,就可以进行对象的序列化和反序列化了。我们可以把序列化的数据用来做什么都可以了。

注意:在反序列化中,必须要显示的调用UserBean实体类,否则会抛出ClassNotFoundException异常。

使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。


但是这里有一点,就是我并不想序列化password字段,因为这是一个敏感的字段。我需要保护起来,不让他进行序列化,那么反序列化就得不到该字段的数值了。

其实这里是可以做的,需要一个关键字transient来修饰不想要序列化的字段就OK了。

    //利用transient关键字修饰,改字段对序列化不可见
    private transient String password;

然后我们就可以在反序列化之后看到password的字段为null了。

//结果:Serializable,objectOutput反序列化: :{userName:yaojt, password:null}

当然,我们还可以自定义序列化来实现上面对某些字段的不序列化操作,而不是使用transient关键字。

在userbean的类中,重写writeObject()和readObject()方法,然后再实现我们想要不被序列化的字段就OK了。重写方法中,显示的调用了out的writeObject()方法,即使字段被transient修饰,也会序列化此字段。

注意:out.defaultWriteObject();和in.defaultReadObject();必须写在最前面,然后再去实现我们想要的操作。在下面的代码中,transient关键字就不起作用了。

private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeObject(password);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        password = (String)in.readObject();
    }

Externalizable类进行序列化

当然,出了继承类进行序列化和反序列化,我们还可以继承Externalizable来进行序列化和反序列化。Externalizable进行序列化和反序列化会比较麻烦,因为需要重写序列化和反序列化的方法,序列化的细节需要手动完成。当读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。因此,实现Externalizable接口的类必须要提供一个无参的构造器,且它的访问权限为public。

下面的类就是实现Externalizable类进行的序列化和反序列化操作。

首先定义另外一个实体类UserBean2.java,继承自Externalizable类。需要重写writeExternal()和readExternal()方法,及实现细节。

package com.yaojt.sdk.java.bean;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class UserBean2 implements Externalizable {

    //串行化版本统一标识符
    private static final long serialVersionUID = 2L;

    private String userName;
    private String password;
    private int age;

    public UserBean2() {
    }

    public UserBean2(String userName, String password, int age) {
        this.userName = userName;
        this.password = password;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(userName);
        out.writeObject(password);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        userName = (String) in.readObject();
        password = (String) in.readObject();
        age = in.readInt();
    }

    //一系列的setter和getter方法,省略了

}

说明:在上面的实体类中,我们可以看到重写的两个方法,分别是序列化和反序列化的方法。我们要做的就是在两个方法里面分别对写入每一个需要序列化的字段。ObjectOutput和ObjectInput里面有一系列写入和读出基本数据类型的方法,可按需进行选用。

具体的使用如下:

public void externalizableTest() {
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            UserBean2 userBean = new UserBean2("fishing", "123456", 25);
            userBean.writeExternal(objectOutputStream);
            objectOutputStream.close();

            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
            UserBean2 userBean2 = new UserBean2();
            userBean2.readExternal(objectInputStream);
            CommonLog.logInfo("externalizable类,序列化和反序列化", userBean2.toString());
            //结果:externalizable类,序列化和反序列化: :{userName:fishing, password:123456}
            objectInputStream.close();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

序列化和反序列化得到的对象是不是同一个?

我们在序列化和反序列化的时候,得到的前后对象是不是相同的呢?答案是否定的。

无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。

如果想要返回一个单例对象,那么改怎么来做呢?
只要我们重写readResolve()方法,构造函数私有化,然后返回唯一的实例。如下所示:

ublic static class InstanceHolder {
        private static final UserBean userBean = new UserBean("tanksu", "999999", 12);
    }

    private Object readResolve() throws ObjectStreamException {
        return InstanceHolder.userBean;
    }

    private UserBean(String userName, String password, int age) {
        this.userName = userName;
        this.password = password;
        this.age = age;
    }

兼容性问题

串行化版本统一标识符-serialVersionUID
java通过一个名为UID(stream unique identifier)来控制,这个UID是隐式的,它通过类名,方法名等诸多因素经过计算而得,理论上是一一映射的关系,也就是唯一的。如果UID不一 样的话,就无法实现反序列化了,并且将会得到InvalidClassException。在继承上面两个接口时,我们并没有看到要实现个UID。默认地,系统帮我们实现了这一个操作。实际上,更推荐自己显示的写一个UID会更好。

向上兼容:指老的版本能够读取新的版本序列化的数据流。因为在java中serialVersionUID是唯一控制着能否反序列化成功的标志,只要这个值不一样,就无法反序列化成功。但只要这个值相同,无论如何都将反序列化,在这个过程中,对于向上兼容性,新数据流中的多余的内容将会被忽略;对于向下兼容性而言,旧的数据流中所包含的所有内容都 将会被恢复,新版本的类中没有涉及到的部分将保持默认值。利用这一特性,可以说,只要我们认为的保持serialVersionUID不变,向上兼容性是 自动实现的。

向下兼容:老的版本能够读取新的数据序列流。有些新的字段可能会没有值,所以需要由一个版本号来进行区分维护,以适应读取不同的数据流的要求。

//串行化版本统一标识符
    private static final long serialVersionUID = 1L;

//初始化version版本号
    private final long version = 2L;

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        if (version == 1L){ //1版本
            out.writeObject(password);
        }else if (version == 2L){ //2版本
            out.writeObject(password);
        }else {
            throw new InvalidClassException(); //抛出异常了
        }
    }

需要满足2个条件
1. serialVersionUID保持一致;
2. 事先实现版本识别标志字段,例如final long version = 2L;

如果抛出异常,就可以提示用户进行升级操作。


总结

序列化和反序列化是一个很有用的用户缓存数据的方式之一。可以跨平台进行传输数据,只要保持UID不变。序列化和反序列化主要有两个序列化接口可以实现,Serializable和Externalizable,可以重写他们的方法进行手动序列化和反序列化操作。对于敏感的字段,可以使用transient关键字进行修饰,那么该字段不会被序列化。对于最后的兼容性问题,是一个比较麻烦的事,需要更好的理解序列化和反序列化的操作。

以上就是所有内容,如有任何问题,请及时与我联系,谢谢。

你可能感兴趣的:(Java)