Serializable接口分析

194 阅读7分钟

Serializable接口概述

  Serializable接口是一个定义在java.io包下的一个语义接口。实现该接口的类都可以进行序列化或反序列化操作(即将对象转化成字节流或将字节流转回对象的操作)。之所以提供这么一个作用,就是因为在java程序在底层操作时,数据都是字节流的形式。而对象作为java中数据的基本载体,为了在底层实现对数据的读取操作,就需要一个方式来使得对象和字节流之间可以互相转换。而Serializable接口就提供了这个功能。

序列化与反序列化

  前面提到,将对象转化成字节流的过程,称为序列化,而将字节流转化为对象的过程,称为反序列化。在java中,ObjectOutputStream和ObjectInputStream这两个类可以帮助我们方便的数据的序列化。因此,在下文的探讨中,我预先实现了两个工具类用于对象的序列化和反序列化。如下:

//将对象保存在指定路径下,文件名为类名
public static void wirteObject(Object o) throws FileNotFoundException, IOException {
	String filename = o.getClass().getName();
	File file = new File(BASE_PATH + filename);
	ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
	oos.writeObject(o);
	oos.close();
}
//将数据从文件中读取,转化为对象
public static Object readObject(Class c) throws FileNotFoundException, IOException, ClassNotFoundException {
	String filename = c.getName();
	File file = new File(BASE_PATH + filename);
	ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
	Object o = ois.readObject();
	ois.close();
	return o;
}

其中,BASE_PATH是我指定的一个路径,是一个常量字符串。 public static final String BASE_PATH = "D:\\mytest\\";

case 1:没有实现Serializable接口的类是否能被序列化

首先,定义一个普通类

class User{
	String name;
	int age;
	public User(String name,int age) {
		this.age = age;
		this.name = name;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
    }   
}

执行以下程序,尝试将User类对象转化为文件

User u = new User("小明",13);
wirteObject(u);

输出结果如下;

Exception in thread "main" java.io.NotSerializableException: langanal.stringanal.User
at java.io.ObjectOutputStream.writeObject0(Unknown Source)
at java.io.ObjectOutputStream.writeObject(Unknown Source)
at langanal.stringanal.SerializableAnal.wirteObject(SerializableAnal.java:25)
at langanal.stringanal.SerializableAnal.main(SerializableAnal.java:40)

由输出结果可以看到,没有实现Serializable接口的类,其对象不能被序列化。

case 2:实现Serializable接口,数据序列化后,反序列化回来的对象数据是否与原数据一致?

给原来的User类实现Serializable接口

class User implements Serializable{
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	String name;
	int age;
	public User(String name,int age) {
		this.age = age;
		this.name = name;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

执行以下程序,并查看结果

User u = new User("小明",13);
wirteObject(u);
u = (User)readObject(User.class);
System.out.println(u.getName()); //小明
System.out.println(u.getAge()); //13

输出结果与注释中一致,表明数据反序列化后,对象数据与序列化之前的对象数据一致

case 3:实现Serializable接口类的子类,一样可以实现序列化

定义一个case2中User的子类SubUser

class SubUser extends User{
	int state;
	public SubUser(String name,int age,int state) {
		super(name,age);
		this.state = state;
	}
	public int getState() {
		return state;
	}
	public void setState(int state) {
		this.state = state;
	}
}

运行以下程序,结果与注释中一致

SubUser su = new SubUser("小明",13,1);
wirteObject(su);
su = (SubUser)readObject(SubUser.class);
System.out.println(su.getName()); //小明
System.out.println(su.getAge()); //13
System.out.println(su.getState()); //1

case 4:某个没有实现Serializable接口的类,当它的子类实现了Serializable接口时,子类可以在序列化操作中执行父类的初始化,但前提是父类拥有不带参的构造方法,如果父类没有相关方法,则会在运行时抛出异常

User类取消实现Serializable接口,SubUser实现Serializable接口,其他保持不变,即,父类没有无参构造方法

class User{
    String name;
    int age;
    public User(String name,int age) {
    	this.age = age;
    	this.name = name;
    }
    public String getName() {
    	return name;
    }
    public void setName(String name) {
    	this.name = name;
    }
    public int getAge() {
    	return age;
    }
    public void setAge(int age) {
    	this.age = age;
    }
}

    class SubUser extends User implements Serializable{
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	int state;
	public SubUser(String name,int age,int state) {
		super(name,age);
		this.state = state;
	}
	public int getState() {
		return state;
	}
	public void setState(int state) {
		this.state = state;
	}
}

运行如下程序

SubUser su = new SubUser("小明",13,1);
try {
	wirteObject(su);
} catch (IOException e) {
	System.out.println("序列化执行失败");
	e.printStackTrace();
}
try {
	su = (SubUser)readObject(SubUser.class);
} catch (ClassNotFoundException | IOException e) {
	System.out.println("反序列化执行失败");
	e.printStackTrace();
}

输出如下

反序列化执行失败
java.io.InvalidClassException: langanal.stringanal.SubUser; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at langanal.stringanal.SerializableAnal.readObject(SerializableAnal.java:33)
at langanal.stringanal.SerializableAnal.main(SerializableAnal.java:47)

从输出中,也可以看到,虽然子类实现了序列化操作,但在反序列化过程中,因为父类没有实现无参构造器,所以导致程序抛出异常。 将User类修改如下:

class User{
	String name;
	int age;
	//新添加的无参构造器
	public User() {
		this.name = "小明(无参构造)";
		this.age = 999;
	}
	public User(String name,int age) {
		this.age = age;
		this.name = name;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

运行如下程序,输出与注释一致:

SubUser su = new SubUser("小明",13,1);
try {
	wirteObject(su);
} catch (IOException e) {
	System.out.println("序列化执行失败");
	e.printStackTrace();
}
try {
	su = (SubUser)readObject(SubUser.class);
} catch (ClassNotFoundException | IOException e) {
	System.out.println("反序列化执行失败");
	e.printStackTrace();
}
System.out.println(su.getName()); //小明(无参构造)
System.out.println(su.getAge()); //999
System.out.println(su.getState()); //1

由以上输出也可以看出,在反序列化过程中,父类的构建由父类的无参构造器完成,而父类原先的数据不会还原,但相对的,实现了序列化的子类,依然会还原原先数据。另外,还需要强调的是,父类的无参构造方法,对子类必须可见,如果将父类构造方法设置成private等子类不可见性式,程序将会报出如下异常:

java.io.InvalidClassException: langanal.stringanal.SubUser; no valid constructor
at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at langanal.stringanal.SerializableAnal.readObject(SerializableAnal.java:33)
at langanal.stringanal.SerializableAnal.main(SerializableAnal.java:47)

case 5:readObject和writeObject方法的使用

首先声明,这两个方法并非在文章开头定义的方法,且为了防止混淆,在下面的代码中,工具方法更名为readObject_util以及writeObject_util 测验如下: 定义一个SpecialClass类,类中包含三个String成员变量,分别为static和transient符号修饰,以及不加任何修饰的String变量

class SpecialClass implements Serializable{
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	static String static_str;
	String normal_str;
	transient String transient_str;
	public SpecialClass(String static_str,String normal_str,String transient_str) {
		this.static_str = static_str;
		this.normal_str = normal_str;
		this.transient_str = transient_str;
		
	}
	public SpecialClass() {
		this.static_str = "static";
		this.normal_str = "normal";
		this.transient_str = "transient";
	}
	public static String getStatic_str() {
		return static_str;
	}
	public static void setStatic_str(String static_str) {
		SpecialClass.static_str = static_str;
	}
	public String getNormal_str() {
		return normal_str;
	}
	public void setNormal_str(String normal_str) {
		this.normal_str = normal_str;
	}
	public String getTransient_str() {
		return transient_str;
	}
	public void setTransient_str(String transient_str) {
		this.transient_str = transient_str;
	}
	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
		this.static_str = (String)in.readObject();
		this.transient_str = (String)in.readObject();
	}
	private void writeObject(ObjectOutputStream out)throws IOException{
		out.writeObject(static_str);
		out.writeObject(transient_str);
	}
}

运行如下程序:

SpecialClass sc = new SpecialClass("static_change","normal_change","transient_change");
System.out.println("-------序列化操作之前------------------");
System.out.println(sc.getStatic_str()); 
System.out.println(sc.getNormal_str()); 
System.out.println(sc.getTransient_str()); 
try {
	wirteObject_util(sc);
} catch (IOException e) {
	System.out.println("序列化执行失败");
	e.printStackTrace();
}
try {
	sc = (SpecialClass)readObject_util(SpecialClass.class);
} catch (ClassNotFoundException | IOException e) {
	System.out.println("反序列化执行失败");
	e.printStackTrace();
}
System.out.println("-------序列化操作之后------------------");
System.out.println(sc.getStatic_str()); 
System.out.println(sc.getNormal_str()); 
System.out.println(sc.getTransient_str()); 

输出如下:

-------序列化操作之前------------------
static_change
normal_change
transient_change
-------序列化操作之后------------------
static_change
null
transient_change

可以看到,在输出中,输出了定义在readObject和writeObjet中保存和读取的变量,但对于没有定义在其中的普通String成员变量,并没有参与到序列化的过程当中,也就是说,这两个方法覆盖了Serializable接口的默认序列化操作,并且提供了更细化的操作。另外,在SpecialClass类中注释掉readObject和writeObject方法后,再次执行上面的程序,会得到如下输出:

-------序列化操作之前------------------
static_change
normal_change
transient_change
-------序列化操作之后------------------
static_change
normal_change
null

可以直观的看到,transient修饰的字段不参与Serializable接口的默认序列化流程,所以输出为null,但是到目前为止,无法确定static修饰的字段是否参与序列化过程。因此,修改运行程序如下:

SpecialClass sc = null;
try {
	sc = (SpecialClass)readObject_util(SpecialClass.class);
} catch (ClassNotFoundException | IOException e) {
	System.out.println("反序列化执行失败");
	e.printStackTrace();
}
System.out.println("-------序列化操作之后------------------");
System.out.println(sc.getStatic_str()); 
System.out.println(sc.getNormal_str()); 
System.out.println(sc.getTransient_str()); 

这里要注意的是,运行这个程序的前提是已经按照上面的步骤将对象保存为对应的文件,不然会抛出java.io.FileNotFoundException异常 最后输出结果如下:

-------序列化操作之后------------------
null
normal_change
null

由以上结果可以得出,static修饰的字段,也默认不参与序列化的流程。
最后,需要注意的是,这两个方法如果要一起写,readObject方法中数据的读取顺序一定要和writeObject方法中数据写入顺序一致。

case 6:探究serialVersionUID用法

java文档强烈建议,在实现Serializable接口时,都要显示指定serialVersionUID的值,具体是为什么呢? 首先,设定一个类,实现Serializable接口,但不显示指定serialVersionUID的值

class TestUID implements Serializable{
	int old;
	public TestUID(int old) {
		this.old = old;
	}
	public int getOld() {
		return old;
	}
	public void setOld(int old) {
		this.old = old;
	}
}

运行如下程序,输入结果与注释一致:

TestUID t = new TestUID(100);
wirteObject_util(t);
t = (TestUID) readObject_util(TestUID.class);
System.out.println(t.getOld()); //100

注释掉wirteObject_util(t)这一句,并将TestUID更新如下:

class TestUID implements Serializable{
	int old;
	int newnum;
	public TestUID(int old) {
		this.old = old;
	}
	public int getOld() {
		return old;
	}
	public void setOld(int old) {
		this.old = old;
	}
	public int getNewnum() {
		return newnum;
	}
	public void setNewnum(int newnum) {
		this.newnum = newnum;
	}
}

运行程序得到报错:

Exception in thread "main" java.io.InvalidClassException: langanal.stringanal.TestUID; local class incompatible: stream classdesc serialVersionUID = 3818576969179375793, local class serialVersionUID = -924220239598009211
at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
at java.io.ObjectInputStream.readClassDesc(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at langanal.stringanal.SerializableAnal.readObject_util(SerializableAnal.java:33)
at langanal.stringanal.SerializableAnal.main(SerializableAnal.java:63)

根据官方文档的说法:

If a serializable class does not explicitly declare a serialVersionUID, then
  the serialization runtime will calculate a default serialVersionUID value
  for that class based on various aspects of the class, as described in the
  Java(TM) Object Serialization Specification.

表明,类对象的变化,直接影响到了serialVersionUID的值的变化,也就导致了异常的报错,关于serialVersionUID的作用,官方规定:“如果接收方加载了一个具有不同于相应发件人类的serialVersionUID的对象的类,则反序列化将导致InvalidClassException 。”也就是说,在类发生变化之后,之前没有显示指定的serialVersionUID发生了变化,与之前已经序列化到文件中的对象的serialVersionUI不一致,所以导致反序列化失败。
按照上述规定,再做一遍实验,首先指定TestUID的serialVersionUID值

class TestUID implements Serializable{
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	int old;
	public TestUID(int old) {
		this.old = old;
	}
	public int getOld() {
		return old;
	}
	public void setOld(int old) {
		this.old = old;
	}
}

将以上类的对象序列化保存到文件后,更改类如下:

class TestUID implements Serializable{
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	int old;
	int newnum;
	public TestUID(int old) {
		this.old = old;
	}
	public int getOld() {
		return old;
	}
	public void setOld(int old) {
		this.old = old;
	}
	public int getNewnum() {
		return newnum;
	}
	public void setNewnum(int newnum) {
		this.newnum = newnum;
	}
}

反序列化之前的文件,运行以下程序,输出与注释一致,也就表明了,在指定了serialVersionUID值的情况下,改变原先类的结构,不会对反序列化原先类对象造成影响:

TestUID t = (TestUID) readObject_util(TestUID.class);
System.out.println(t.getOld()); //100