Java IO流:(十二)序列化与反序列化

一、序列化概述

  Java 提供了一种对象 序列化 的机制。用一个字节序列可以表示一个对象,该字节序列包含该 对象的数据、对象的类型 和 对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中 持久保存 了一个对象的信息。

  反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。 对象的数据 、 对象的类型 和 对象中存储的数据信息,都可以用来在内存中创建对象。 

  图解序列化

  序列化和反序列化的概述

二、对象的序列化

  对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。 当其它程序获取了这种二进制流,就可以恢复成原来的Java对象。

  序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据使其在保存和传输时可被还原。

  序列化是 RMIRemote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI JavaEE 的基础。因此序列化机制是JavaEE 平台的基础。

  如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。
否则,会抛出NotSerializableException异常。

Serializable
Externalizable

  

  凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量:

private static final long serialVersionUID;

  serialVersionUID 用来表明类的不同版本间的兼容性。 简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。

  如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的若类的实例变量做了修改, serialVersionUID 可能发生变化。 故建议,显式声明

  简单来说, Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时, JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。 (InvalidCastException)


  

三、序列化与反序列化

  1、概述

    序列化:简单理解就是把程序里面生成的对象以文件的形式保存到本地硬盘中,序列化写入文件的IO是ObjectOutputStream流。

    反序列化:就是把序列化的对象文件导入到程序中,并生成为一个对象,供程序使用。反序列化的读取对象文件的IO流是 ObjectInputStream

    Java 中,经典的方式实现对象序列化,可以实现序列化接口。

  2、哪些属性不序列化

    static 属性不会被序列化,反序列化时恢复为当前类变量的值。

      因为 static 代表“一个类一个”,而不是“一个对象一个”,因此它不是对象的状态。

    如果某个实例变量不能或不应该被序列化,就把它标记为 transient(瞬时)的。

      当某个属性的类型没有实现 Serializable 接口,而又不能修改该类,那么该实例变量不能被实例化。

      那么只能把该属性标记为 transient 。或者动态数据只可以在执行时求出而不能或不必存储。

      被标记为 transient 的属性,在反序列化时,被恢复为默认值

  3、序列化版本ID

     如果你将对象序列化,则必须要有该类才能还原和使用该对象。但若同时又修改了类会发生什么事?

     有些修改会严重违反Java的类型安全性和兼容性,例如:删除了某个实例变量、改变实例变量的类型、将类从可序列化修改为不可序列化,将实例变量修改静态的等等。

     但是有些修改通常不会影响,例如:加入新的实例变量(还原时可以按默认值处理),将实例变量从瞬时修改为非瞬时的(可以使用默认值)等。

      但是,现在所有对类的修改都导致原来的数据在反序列化时失败 java.io.InvalidClassException。

     解决这个问题的方法,就是在实现 java.io.Serializable 接口时,增加一个long类型的静态常量 serialVersionUID。如果类没有显示定义这个静态变量,它的值是Java运行时环境根据类的内部细节自动生成的,若类的源代码作了修改,serialVersionUID 就会发生变化,从而导致“旧”数据反序列化失败。

     如果对类做了会影响数据兼容性的操作时,要么修改serialVersionUID的值,使得原来的反序列化数据失败,要么你要对“旧”对象反序列化后引起的问题负责。

  4、

  5、

四、java.io.Serializable 接口

  1、作用

    Java 类通过实现 java.io.Serializable 接口和定义 serialVersionUID常量 以启用其序列化功能,未实现此接口的类型将无法使其任何状态序列化或反序列化。

  2、Serializable 接口

    Serializable接口:只是一个标记,JVM在序列化的时候,会去判断要序列化的对象是否有实现Serializable接口,如果没有实现就会报错,不允许系列化。

    序列化接口没有方法或字段,仅用于标识可序列化的语义。

    如果实现 Serializable 接口,对象如何序列化,各个属性序列化的顺序是什么,都是默认的,程序员本身无法指定,也不用关心。

  3、serialVersionUID常量

    serialVersionUID 是指JVM在序列化对象的时候,会把这个常量表示序列化对象所属的类的类ID。在反序列化时,反序列化对象的 serialVersionUID 能匹配上程序里面的类的 serialVersionUID 时,就判断这个反序列化的对象就是这个类生成的,因此允许反序列化。

  4、序列化注意点

    可序列化类的所有子类型本身都是可序列化的。

    如果属性前面有  static 和 transient 修饰,该属性不参与序列化。

    如果需要序列化对象本身的属性,那么该属性的类也要实现Serializable接口,否则不能序列化。

  5、Serializable 接口的其他认识

    (1)实现了Serializable接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。 这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异换句话说,可以先在Windows机器上创建一个对象,对其序列化,然后通过网络发给一台Unix机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。

    (2)由于大部分作为参数的类如StringInteger等都实现了java.io.Serializable的接口,也可以利用多态的性质,作为参数使接口更灵活。

  6、

  7、

五、java.io.Externalizable 接口

  若某个要完全控制某一对象及其超类型的流格式和内容,则它要实现 Externalizable 接口中的 writeExternal 和 readExternal 方法。

  程序员要在 writerExternal 方法中,自己定制哪些属性要序列化,顺序是什么样的。

  程序员要在 readExternal 方法中,自己定制哪些属性要反序列化,顺序与 writerExternal 方法中的一致。

  Demo:

 1 import java.io.Externalizable;
 2 import java.io.IOException;
 3 import java.io.ObjectInput;
 4 import java.io.ObjectOutput;
 5 
 6 public class Goods implements Externalizable{
 7     private static String brand = "Made In China";
 8     private String name;
 9     private double price;
10     private transient int sale;
11     public Goods(String name, double price, int sale) {
12         super();
13         this.name = name;
14         this.price = price;
15         this.sale = sale;
16     }
17     public Goods() {
18         super();
19     }
20     public static String getBrand() {
21         return brand;
22     }
23     public static void setBrand(String brand) {
24         Goods.brand = brand;
25     }
26     public String getName() {
27         return name;
28     }
29     public void setName(String name) {
30         this.name = name;
31     }
32     public double getPrice() {
33         return price;
34     }
35     public void setPrice(double price) {
36         this.price = price;
37     }
38     public int getSale() {
39         return sale;
40     }
41     public void setSale(int sale) {
42         this.sale = sale;
43     }
44     @Override
45     public String toString() {
46         return "Goods [brand = " + brand +",name=" + name + ", price=" + price + ",sale = " + sale +"]";
47     }
48     @Override
49     public void writeExternal(ObjectOutput out) throws IOException {
50         //程序员自己定制要序列化的内容,顺序等
51         //这两个方法是在对象被序列化和反序列化的过程中,JVM帮我们调用
52         out.writeUTF(brand);//静态的也序列化
53         out.writeUTF(name);
54         out.writeDouble(price);
55         out.writeInt(sale);//有transient也序列化
56     }
57     @Override
58     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
59         //程序员自己定制要反序列化的内容,顺序等,建议与序列化的顺序一致
60         brand = in.readUTF();
61         name = in.readUTF();
62         price = in.readDouble();
63         sale = in.readInt();
64     }
65     
66 }

  序列化与反序列化:

 1 import java.io.FileInputStream;
 2 import java.io.FileOutputStream;
 3 import java.io.IOException;
 4 import java.io.ObjectInputStream;
 5 import java.io.ObjectOutputStream;
 6 import org.junit.Test;
 7 
 8 public class TestObject {
 9     @Test
10     public void test01() throws IOException{
11         Goods goods = new Goods("《Java从入门到放弃》", 20.45, 100);
12         Goods.setBrand("China Beijing");
13         
14         FileOutputStream fos = new FileOutputStream("goods.dat");
15         ObjectOutputStream oos = new ObjectOutputStream(fos);
16         
17         oos.writeObject(goods);
18         
19         oos.close();
20         fos.close();
21     }
22     
23     @Test
24     public void test02()throws IOException, ClassNotFoundException{
25         FileInputStream fis = new FileInputStream("goods.dat");
26         ObjectInputStream ois = new ObjectInputStream(fis);
27         
28         Object obj = ois.readObject();
29         System.out.println(obj);
30         
31         ois.close();
32         fis.close();
33     }
34 }

六、小结

    对于实现序列化,有两种方式,一种是经典的 Serializable 接口,另外一种是实现 Externalizable接口。

    1.  Serializable 是自动序列化,因此直接编写序列化编号即可,如果需要非序列化 transient 属性,就需要重写writeObject(ObjectOutputStream s)、readObject(ObjectInputStream s),原则与下面一致。

    2.  Externalizable 不是自动序列化,需要重写 writeExternal(ObjectOutput out)、readExternal(ObjectInput in)方法,除此外还需要对象有空的构造函数。

     (1)writeExternal、readExternal可以指定具体的序列化的属性。写一个序列化一个属性。

     (2)序列化属性和反序列化,都是按照顺序的。

    3.  其实实现这两个接口是一样效果的,只是Serializable可以自动序列化非transient的变量,而Externalizable不会序列化任何变量。两者需要序列化transient都需要重写一些特定方法。 而Serializable的方法是JVM默认的方法,Externalizable则是接口定义的方法。

    4.  序列化数组,反序列化也要使用集合对接,否则会报类转义异常。

    5.  更改了类名以后,无法进行反序列化。

原文地址:https://www.cnblogs.com/niujifei/p/14876387.html