Java序列化简述

Posted by xionghongzhi on September 9, 2017

最近几天都没有时间写博客了,其实也没有什么好说的。

连续两周加班,效率超级低。天天一点屁事还要做的很晚,关键是对系统实在是不熟悉。真的得花点时间在PIKE上了。

要不然,真的每次做一点界面就是一天,还各种BUG。

总结一下最近的关键问题:写代码命名不规范,效率低,代码需要多多进行抽象,封装。能复用的尽量多复用。

因为这段时间敲代码,感觉一直都是停留在很低的一个层次,比CV高一点吧。所以感觉到Java基础还是不扎实。

真觉得自己心比天高,想学这个,想学那个。结果自己吃饭的本事还学的不扎实。

所以打算今后一段之间,空闲的时候,恶补一下Java基础吧。

我总是这样。说出的话与做出的事总有差别。尽量规正吧。


言归正传,私以为,当下很多博客都有CV之嫌,那句话怎么说的,我们不生产代码,我们只是代码的搬运工

昨天看源码的时候,突然看到了一个经常会被我忽略的东西,serialization序列化。

细想一下,又想不起来了序列化和反序列化的作用。

什么是序列化?

从广义上来说,Java序列化是为了持久化存储对象状态的一种方式。它指的是能够把堆内存中的Java对象数据,通过某种方式存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。

反序列化就是上述步骤相反的过程,把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象的过程。

为什么要进行序列化?

  1. 分布式系统中,需要共享数据的实体对象都需要进行序列化,因为此时需要将对象在网络上进行传输,需要将对象转换成二进制的形式。 在Java中,想要进行序列化,就必须实现Serializable接口,这个接口没有任何的方法,它就是一个标志接口,只要实现了这个接口,那么你就可以开始你的表演了,序列化。

  2. 服务钝化: 听说过Session钝化和活化么? 其实不就是把不怎么使用的对象由内存堆中序列化到磁盘空间上。 这整个过程就是将Java对象,转换成二进制文件。如果序列化之后,还需要使用这个对象,先会从内存堆中去找,如果找不到,就再会去相应的磁盘空间中去找,如果找到了,就会将其由二进制文件再次转换成Java对象。

在Java中,提供了序列化以及反序列化的类: java.io.ObjectOutputStream 以及 java.io.ObjectInputStream

下面,看一个序列化的例子,这是一个普通的不能再普通的例子了:

public class ObjectOutputSteamDemo {
    public static void main(String[] args) {
        File serialization = new File("./serizaliable.txt");
        serializationOperation(serialization);

    }

    /**
    * 
    * method desc 该方法用于进行序列化操作,将Java对象通过流的方式存储在磁盘文件上
    *
    * @param serialization:
    *            存放对象数据的文件
    */
    private static void serializationOperation(File serialization) {
        ObjectOutputStream outputStream = null;
        try {
            outputStream = new ObjectOutputStream(new FileOutputStream(serialization));
            outputStream.writeObject(new Programmer("xiaoxiong", "12345", 18));
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

为了方便操作,创建了一个实体对象,用于供ObjectOutputSteam进行writeObjec操作:

public class Programmer {
    private String name;
    private String password;
    private int age;

    public Programmer(String name, String password, int age) {
        super();
        this.name = name;
        this.password = password;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Programmer [name=" + name + ", password=" + password + ", age=" + age + "]";
    }
}

自行导入类,编译运行,你会发现得到了一个运行时异常错误:

java.io.NotSerializableException: com.hardstudy.entity.Programmer
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at com.hardstudy.io.serialization.ObjectOutputSteamDemo.serializationOperation(ObjectOutputSteamDemo.java:39)
    at com.hardstudy.io.serialization.ObjectOutputSteamDemo.main(ObjectOutputSteamDemo.java:24)

这个异常是怎么出现的呢? debug一下就不难发现,出现这个异常的原因是Programmer类没有实现Serializable接口。

稍微变化一下,给我们的实体类增加一个Serializable接口,再次运行,你会发现,成功运行并将内容写入磁盘文件serialization.txt中。不过都是写乱码,这是供反序列化的时候读取的。

下面再看看怎么把数据从磁盘中读取出来:

/**
    * 
    * method desc 反序列化,将磁盘文件中的内容读取成Java对象
    *
    * @param serialization
    */
    private static void deserializationOperation(File serialization) {
        try {
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(serialization));
            Programmer programmer = (Programmer) inputStream.readObject();
            System.out.println(programmer);
            inputStream.close();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

其实很好理解,序列化以及反序列化就是两个相反的行为,一个是将内容从程序中读到外部,且不管是外部指的是磁盘文件还是其他不同的网段上,这在Java中就需要使用输出流。而反序列化则是将外部的内容读取成Java内存(内部)的对象,因此使用输入流

总结一下: 需要做序列化的对象的类必须要实现Serializable这个标志接口,底层它会进行判断,this.getClass().isAssignableFrom(Serializable.class); (还有一个instanceOf 也可以用来判断) 如果为真,则会进行序列化操作。 这也就是这个标志接口的作用。

Java中大部分类都实现了Serializable,如,String,Integer这类经常用来传递数据的类。

transient关键字

在Java的Serializable接口的API中,明确的声明了一下几点:

The readObject method is responsible for reading from the stream and restoring the classes fields. It may call in.defaultReadObject to invoke the default mechanism for restoring the object's non-static and non-transient fields. 

第一点,ObjectInputStream的readObject方法将只会读取并存储那些非静态以及非瞬时的属性,这其实是非常有效的,比如,我们并不像把Password这种数据直接反序列化出来,那么此时的transient就非常有效。

下面,把Programmer类的Password改成transient形式:

private String name;
transient private String password;
private int age;

再次编译的时候,就会发现一个错误:

java.io.InvalidClassException: com.hardstudy.entity.Programmer; local class incompatible: stream classdesc serialVersionUID = 6334356052556574577, local class serialVersionUID = -8942116313638965447

这是为什么呢?看看Serializable接口的API

反序列化Java对象时必须提供对象的class文件,问题是,随着项目的升级,系统的class文件也会随之变化(增加一个字段或者删除一个字段),如何保证两个class文件的兼容性呢? Java通过serialVersionUID序列化版本号来判断字节码是否发生变化。

如果没有显示的定义序列化版本号,那么JVM会自动根据类相关信息为我们创建出来,而修改之后的类往往得到其他的版本号,因此,在反序列化的时候,两个版本匹配不了,就会出现java.io.InvalidClassException异常。

注意,这里需要明白,在序列化操作的时候,JVM已经为我们创建了版本号,并且帮我们已经放进了文件当中去,这就是说,这文件,只有我这个版本的才能反序列化出来,其他的没门!!! 那么下次变化了文件,JVM又帮我们生成其他一个不同的版本号,拿着这个版本号去反序列原先那个磁盘文件(假设), 那不就是相当于拿你家钥匙开别家的门吗?

所以,都加上一个Long的序列号,无论我怎么改,我家的钥匙永远都能开自己家的门。

因此,最好建议在实现了Serializable接口之后,提供一个固定的UID,这样就可以防止出现一系列的问题。

private static final long serialVersionUID = 1L;  // 加入序列号
private String name;
transient private String password;
private int age;

解决了这个异常,继续编译,你就会发现,在反序列化的时候,这个被transient修饰的属性隐藏掉了。

这就是序列化与反序列化的一些介绍,内容其实不难,了解序列化的作用,为了更好的使用对象(将对象转换成二进制)以及对象的钝化(有效减少堆内存),还有序列号版本问题以及transient关键字的使用,有效避免一些字段的序列化操作。