在Eclipse中,一个对象如果实现了Serializable接口,会有一个警告信息:
The serializable class Student does not declare a static final serialVersionUID field of type long
点击警告信息,会给出3个修复建议,其中第二个是:
add generated serial version id
点击此建议,就会自动生成如下代码(具体数值不定):
private static final long serialVersionUID = 6859324283664879676L;
这个值是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段。
那么这个常量serialVersionUID有什么作用呢?如果忽略这个警告信息,不定义这个常量又会如何?
先来明确一下对象序列化的含义和用途:
把Java对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为Java对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
(1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
(2) 在网络上以字节序列的方式传输对象。
我们以第一种用途为例,用具体代码测试serialVersionUID的作用
代码段一:类Student version1.0,这个类将会被序列化保存到文件中
/**
* Student
* @version 1.0
*/
public class Student implements Serializable {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "姓名:" + name + " 年龄:" + age;
}
}
代码段二:方法write(序列化)
/**
* 将一个对象数据写入到一个二进制文件
*
*/
private static void write(){
try {
File file = new File("student_object.dat");
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
Student std = new Student("Tom", 15);
oos.writeObject(std);
oos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
代码段三:方法read(反序列化)
/**
* 从一个二进制文件中读取对象数据并打印对象信息
*
*/
private static void read(){
try {
File file = new File("student_object.dat");
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
System.out.println(obj);
ois.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
代码段四:调用
public static void main(String[] args) {
write();
read();
}
运行结果:姓名:Tom 年龄:15
可以看到,在没有定义serialVersionUID这个常量的情况下,序列化和反序列化过程都没有问题。
但是,如果软件升级,Studen类进行了扩展,加入了一个变量:number(学号),如下
代码段五:类Student version2.0
/**
* Student
* @version 2.0
*/
public class Student implements Serializable {
private String name;
private int age;
private int number;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student(String name, int age, int number) {
this.name = name;
this.age = age;
this.number = number;
}
@Override
public String toString() {
return "姓名:" + name + " 年龄:" + age + " 学号:" + number;
}
}
这个时候,再调用read方法读取之前保存的student_object.dat 文件,就会导致兼容性问题,因为要把旧版的Student反序列化成新版的Student,而新版中多了一个变量,就会抛出异常信息:
java.io.InvalidClassException: com.test.io.Student; local class incompatible: stream classdesc serialVersionUID = 6859324283664879676, local class serialVersionUID = -5807139062119555074
意思是从流中读取的类Student(v1.0)的serialVersionUID和本地类Student(v2.0)的serialVersionUID不一致。
好,这时候serialVersionUID终于出场了^_^
前面说过,这个值是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段。它其实起了一个在文件内部声明版本的作用,前面因为我们忽略了Eclipse的警告,没有定义这个常量serialVersionUID,所以,虚拟机会分别计算出stream中读取的类的serialVersionUID和本地对应的类的serialVersionUID,然后两者做对比,如果一致,则认为是同一版本,继续反序列化操作;如果不一致,则认为是不同版本,序列化过程可能会丢失文件信息,所以抛出java.io.InvalidClassException 异常。
那么如何避免此类问题呢?就是听从Eclipse的建议,在Student类中自己指定或者自动生成一个serialVersionUID,如下:
代码段:类Student version 1.1
/**
* Student
* @version 1.1
*/
public class Student implements Serializable {
private static final long serialVersionUID = 6859324283664879676L;
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "姓名:" + name + " 年龄:" + age;
}
}
调用上面的write方法,将定义了serialVersionUID的Student写入文件中,稍后再用。
然后软件升级,Student类扩展,加入变量:number(学号),
同时:保持serialVersionUID的值不变
如下:
/**
* Student
* @version 2.1
*/
public class Student implements Serializable {
private static final long serialVersionUID = 6859324283664879676L;
private String name;
private int age;
private int number;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public Student(String name, int age, int number) {
this.name = name;
this.age = age;
this.number = number;
}
@Override
public String toString() {
return "姓名:" + name + " 年龄:" + age + " 学号:" + number;
}
}
然后调用之前的read方法,读取刚才生成的 Student v 1.1版的文件
运行结果:姓名:Tom 年龄:15 学号:0
测试结果表明,在定义了同一个serialVersionUID值之后,类做了扩展的情况下,依然保持了兼容性,能将老版本时生成的序列化文件反序列化为新版的类,只是其中新增的的变量为默认值。
简言之:serialVersionUID 的作用就是保持兼容性。
扩展问题一:
在read方法中对文件进行反序列化操作的时候,java虚拟机怎么知道要把文件中的数据转换成Student类呢?还打印出了Student类的相关信息?
答案就在文件中,用EditPlus打开序列化文件 student_object.dat,选择“16进制查看器”,文件内容显示如下:
可以看到,文件中包含了包名、类名等信息,所以java虚拟机会按图索骥,找到相关的本地类,然后进行反序列化
如果随便用一个文本文件,改名为student_object.dat,然后读取,会导致转换失败,抛出IOException。
扩展问题二:
上面所说的“软件升级,类扩展”还有很多方式,比如:
1. 如果用2.1版的Student 写入文件,再用1.1版的Student 来读取,会出现什么结果?
2. 如果Student 类扩展的时候没有新增字段,而是修改了已有的变量名或变量类型,又会出现什么结果?
类似的可能还有很多种,有兴趣的朋友可以自己试试。