豆角茄子子

导航

Java序列化Serializable的那些事儿

说到Java的序列化,有个问题就是为什么需要序列化,更优先的一个问题是什么是序列化。

序列化的含义

《Java编程思想》中这么解释,Java的对象序列化是将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。

换句话说就是,主要将对象的可变信息以字节序列,保存在硬盘文件、数据库或者通过网络传输到另一个JVM中等等,等到需要在内存中恢复该对象当时的状态时,也就是当别的机器或程序运行时需要该对象的状态时,可以反序列化这些字节序列,将此对象还原出来使用(也说明了序列化是可以跨操作系统和JVM的)。这种机制就叫做序列化。

序列化的用途

明白了序列化的含义,也不难清楚序列化的用途了。

  • 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
  • 当你想用套接字在网络上传送对象的时候;
  • 当你想通过RMI传输对象的时候。

明白了序列化的含义及用途,接下来需要了解序列化的使用。

序列化的使用

package com.szh.serializable;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class TestSerializable {

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		Person p1 = new Person();
		Dog d1 = new Dog();
		d1.setName("Dog1");
		p1.setId(1);
		p1.setName("Tom");
		p1.setDog(d1);
		// serializable
		if (!new File("E:/tests").exists()) {
			new File("E:/tests").mkdirs();
		}
		FileOutputStream fos = new FileOutputStream(new File("E:/tests/serializable_file.ser"));
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(p1);
		oos.close();
		
		// deserializable
		FileInputStream fis = new FileInputStream(new File("E:/tests/serializable_file.ser"));
		ObjectInputStream ois = new ObjectInputStream(fis);
		Object obj = ois.readObject();
		ois.close();
		Person p = (Person) obj;
		System.out.println(p);
	}
	
	public static class Person implements Serializable {
		private static final long serialVersionUID = -1891426275960796136L;
		private transient int id;
		private String name;
		private transient Dog dog;
		
		public Dog getDog() {
			return dog;
		}
		public void setDog(Dog dog) {
			this.dog = dog;
		}
		public int getId() {
			return id;
		}
		public void setId(int id) {
			this.id = id;
		}
		public String getName() {
			return name;
		}
		public void setName(String name) {
			this.name = name;
		}
		@Override
		public String toString() {
			return "Person [id=" + id + ", name=" + name + ", dog=" + dog + "]";
		}
	}
	
	public static class Dog {
		private String name;

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		@Override
		public String toString() {
			return "Dog [name=" + name + "]";
		}
	}

}

在序列化的使用中,主要有以下几个问题:

  • transient关键字的作用;
  • serialVersionUID的作用;
  • 序列化警告的处理方式。

transient关键字的作用

transient,意为短暂的,临时的。在Java中,transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。

如示例代码中Person拥有一个transient修饰的id和Dog,程序运行后,经过序列化和反序列化,结果如下:

Person [id=0, name=Tom, dog=null]

可以发现,id和Dog可以认为分别为各自类型的缺省值,id=0,dog=null。明显这两个变量未经过序列化过程。

注意:不需要经过序列化的类型的成员变量,使用transient修饰后可以不实现Serializable接口。如Person的Dog不需要序列化。当然,非要给Dog实现Serializable接口也不影响结果。有意思的是,在eclipse中自动生成toString()方法,发现使用了transient修饰的成员变量默认不被选择参与打印对象字符串。

serialVersionUID的作用

当没有显式地定义long类型的serialVersionUID变量时,Java序列化机制会根据编译的class(它通过类名,方法名等诸多因素经过计算而得,理论上是一一映射的关系,也就是唯一的)自动生成一个serialVersionUID作序列化版本比较来使用。

解析serialVersionUID的jdk代码,可调试以下代码:

因此,也可验证没有显式添加serialVersionUID也同样拥有序列号,所以在序列化的时候必然也有jdk代码根据各种因素来生成serialVersionUID。

这种情况下,如果class文件(主要是类名、方法名等)没有发生变化(增加空格、换行、注释等等不会产生变化),就算再编译多次,serialVersionUID也不会变化。但是一旦变化,如给类增加了方法、属性等,那么在反序列化时,就会出现序列化版本不一致的异常(InvalidCastException)!如下:

Exception in thread "main" java.io.InvalidClassException: com.szh.serializable.TestSerializable$Person; local class incompatible: stream classdesc serialVersionUID = -5579678255079137242, local class serialVersionUID = -1463584727334114570
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:617)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1622)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1517)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1771)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
	at com.szh.serializable.TestSerializable.main(TestSerializable.java:30)

因此,serialVersionUID即代表了对应类的版本号,目的是为了保证序列化和反序列化时,类信息的一致性和安全性。

序列化警告的处理方式

在Java中,我们对警告不应无视,每一条警告都应该做合适的处理。那对于序列化时,我们实现了Serializable接口,却未显示定义serialVersionUID时,IDE会提示出三种处理方式:

  • Add default serial version ID;
  • Add generated serial version ID;
  • Add @SuppressWarnings 'serial' to 'Person'。

当我们选择第一种处理方式时,相当于显式地手动定义了类的当前版本(自己去维护)。当我们以后需要此类时,需要同步修改serialVersionUID。

当我们选择第二种处理方式时,相当于显式地自动定义了类的当前版本(根据类信息等内容自动生成)。当我们以后需要此类时,需要重新生成serialVersionUID。

当我们选择第三种处理方式时,相当于告诉编译器忽略此警告。那么问题来了,第三种方式貌似可有可无,毕竟相当于上面哪种都未选择。答案是否定的。

在Java中,任何警告我们都不应不处理(选择使用第三种注解方式忽略警告也认为是一种处理方式,但是三种方式都不选择的话,则认为是不处理),虽然使用@SuppressWarnings去忽略也是一样的运行效果。因为,这是一种很好地编码习惯,有时警告也会造成一些意想不到的问题,所以我们应当处理代码中所有的警告,对于确实可以忽略掉的警告,jdk提供了这个注解来标记代码警告行,这样便可以使我们的代码逻辑更加严密,因为我们处理了每一条警告。

那么我们如何在编译后的class文件中,找到类的版本信息呢?

序列化与字节码class文件

class文件,存放了十六进制字节码,其中包含了类编译时的序列版本号。我们可以使用如下命令进行解析(反编译):

javap -v class文件名 > 输出文件名
如:javap -v E:/J2SE_workspace/MyTest/bin/com/szh/serializable/TestSerializable$Person.class > E:\test.txt

解析后可以看出来,对于第一第二种显式地添加版本号的类来说,能够明显找到serialVersionUID,而对于使用注解忽略或直接忽略的方式,则不能找到serialVersionUID。

不论三种中哪种处理警告的方式,在序列化到文件的这个过程中,此时类的版本号serialVersionUID必然已保存在了字节序列中。

posted on 2018-10-09 00:47  豆角茄子子  阅读(178)  评论(0编辑  收藏  举报