Serializable接口中serialVersionUID的作用 示例

在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 类扩展的时候没有新增字段,而是修改了已有的变量名或变量类型,又会出现什么结果?

类似的可能还有很多种,有兴趣的朋友可以自己试试。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值