Java开发之道
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

陷阱14 不同版本——反序列化对象

在编程的过程中,为了数据的安全,可以使用文件存储数据对象,这样,当需要从文件中读取数据对象时,如果对象的版本不一致,程序将无法从文件中读取数据对象,从而可以有效地保证数据的安全。

在Java中提供了一个java. io. Serializable接口,该接口中没有任何方法,序列化类只需要实现这个接口即可,为了保证对象序列化的版本一致,可以通过在序列化类中定义属性serialVersionUID来实现。

serialVersionUID属性的定义格式如下:

    private static final long serialVersionUID = 长整型常量;

说明

上面代码中的长整型常量可以为任意的长整型数值,如200L,只要为该类指定了属性serialVersionUID,并为其指定了具体的长整型数值,这样,当序列化类的属性发生改变时,就可以保证序列化对象的版本一致了,只有对象的版本一致才能被成功地反序列化。

接下来通过一个实例来说明,序列化对象时为什么要使用serialVersionUID属性来指定对象的版本。

例3.14 创建3个类,分别序列化类、将序列化对象保存到文件中的类和从文件中读取序列化对象的类,目的是测试对象序列化的版本对反序列化的影响。(光盘位置:光盘\MR\Instance\3\14\ UnserialObject)。

1)序列化类Student

    import java. io. Serializable;
    public class Student implements Serializable{// 实现Serializable接口,创建序列化类Student
      ❶//private static final long serialVersionUID = -5943957013018546741L;
      private int id;                          // 定义属性id
      private String name;                     // 定义属性name
      public void setName (String name) {       // 定义属性name的Setter方法
      this. name = name;
      }
      public String getName () {                // 定义属性name的Getter方法
      return name;
      }
      public void setId (int id) {              // 定义属性id的setter方法
      this. id = id;
      }
      public int getId () {                     // 定义属性id的setter方法
                return id;
      }
    }

说明

上面的代码创建了一个实现了Serializable接口的Student类,因此该类是一个序列化类,可以用于创建序列化对象,但是在该类中没有指定serialVersionUID属性,所以该类并不能保证序列化对象的版本一致。

2)向文件中保存序列化对象的类WriteStudent

    import java. io. File;
    import java. io. FileOutputStream;
    import java. io. ObjectOutputStream;
    public class WriteStudent {
        public static void main (String[] args) {
          try{
              Student stud = new Student ();                  // 创建序列化对象stud
              stud. setId (18);                                // 设置对象的id属性值
              stud. setName ("小强");                          // 设置对象的name属性值
              File file = new File ("d:/object. txt");         // 创建d:/object. txt的File对象
              FileOutputStream out = new FileOutputStream (file);     // 创建字节输出流
              ObjectOutputStream obj = new ObjectOutputStream (out);  // 创建对象输出流
              obj. writeObject (stud);                         // 将对象保存到文件中
              System. out. println ("对象保存成功!! ! ");      // 输出对象保存成功的信息
              obj. close ();                                   // 关闭对象输出流
              out. close ();                                   // 关闭字节输出流
            }catch (Exception ex){
              ex. printStackTrace ();                          // 发生异常则打印异常践踪迹
          }
        }
    }

说明

上面的代码创建了一个向文件中保存序列化对象的WriteStudent类,该类用于将Student类创建的序列化对象stud及其属性id和name保存到文件(D:\object. txt)中。

3)从文件中读取序列化对象的类ReadStudent

    import java. io. File;
    import java. io. FileInputStream;
    import java. io. ObjectInputStream;
    public class ReadStudent {
      public static void main (String[] args) {
          try{
              File file = new File ("d:/object. txt");        // 创建d:/object. txt的File对象
              FileInputStream in = new FileInputStream (file);    // 创建字节输入流
              ObjectInputStream obj = new ObjectInputStream (in); // 创建对象输入流
              Student stud = (Student) obj. readObject (); // 从流中读取对象,实现反序列化
              System. out. println (stud. getId ());             // 输出反序列化对象的属性id
              System. out. println (stud. getName ());           // 输出反序列化对象的属性name
              obj. close ();                                  // 关闭对象输入流
              in. close ();                                   // 关闭字节输入流
          }catch (Exception ex){
              ex. printStackTrace ();                         // 发生异常则打印异常践踪迹
          }
        }
    }

说明

上面的代码创建了一个ReadStudent类,该类用于从文件(D:\object. txt)中读取序列化对象,即读取使用WriteStudent类保存的序列化对象stud,进而读取该对象的id属性和name属性。

程序的运行步骤如下:

首次执行程序时,应先执行WriteStudent类,其作用是将Student类创建的stud对象以及其属性id和name保存到文件(D:\object.txt)中,其中属性id的值是“18”, name的值是“小强”,并且执行WriteStudent类还将在控制台显示如图3.7所示的信息,说明成功地将序列化对象保存到文件中了。

图3.7 序列化对象保存成功的提示信息

执行ReadStudent类来测试是否可以从文件(D:\object.txt)中读取序列化对象stud以及其属性id和name,也就是进行反序列化,执行该类将在控制台显示如图3.8所示的信息,从图中可以看到该类成功地从序列化对象stud中读取到其属性id和name的值,分别是“18”和“小强”。

图3.8 反序列化成功

说明

由于是第1次执行程序,所以一定要先执行第1步保存序列化对象,然后再执行第2步从文件中读取序列化对象及其属性,程序执行完上述两步是不会产生任何错误的,这是由于当执行第1步之后,并没有对Student类的属性进行任何更改(即没有在Student类中再添加任何属性,如没有在Student类的代码中添加private int age;这样的语句,也没有删除原有的id和name属性等),所以在执行第2步时,是可以从文件中读取到序列化对象及其属性的。

在Student类中添加属性age,也就是在下面第一段原代码下面添加属性age并保存,添加后的代码如下第二段代码所示。

    private int id;                                // 定义属性id
    private String name;                           // 定义属性name
 
    private int id;                                // 定义属性id
    private String name;                           // 定义属性name
    private int age;                               // 添加新的属性age

直接执行ReadStudent类从序列化对象中读取数据,这时,程序会在控制台输出如图3.9所示的异常信息。

图3.9 反序列化对象时显示的异常提示信息

说明

程序执行完第3步和第4步后,之所以会发生异常,是由于在第3步对Student类的属性进行了更改,即在Student类中添加age属性,所以在反序列化对象时发生了如图3.9所示的序列化对象版本不一致的异常。

技巧

为了避免由于对创建序列化对象的类(如本实例中的Student类)的属性修改后,导致反序列化失败,可以在创建序列化类时添加serialVersionUID属性,并为该属性指定一个长整型数值,来保证序列化对象的版本一致,也就是说,如果在第1步执行程序前将Student类中serialVersionUID 属性所在行的注释取消,即将Student 类中❶标记处的注释取消,这样程序就不会出现上述异常了。