序列化的含义和意义
对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流,都可以将这种二进制流恢复成原来的Java对象。
对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象保存为字节数组,这些字节数组可以保存在磁盘上,或通过网络传输。在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。
在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。通常建议:程序创建的每个JavaBean类都实现Serializable。
相关接口及类
Java为了方便开发人员将Java对象进行序列化及反序列化提供了一套方便的API来支持。其中包括以下接口和类:
java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream
Serializable 接口
类通过实现 java.io.Serializable
接口以启用其序列化功能。未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出 NotSerializableException
。
如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该集成java.io.Serializable
接口。
下面是一个实现了java.io.Serializable
接口的类
package com.hollischaung.serialization.SerializableDemos; import java.io.Serializable; /** * Created by hollis on 16/2/17. * 实现Serializable接口 */ public class User1 implements Serializable { private String name; private int age; 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; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
通过下面的代码进行序列化及反序列化
package com.hollischaung.serialization.SerializableDemos; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import java.io.*; /** * Created by hollis on 16/2/17. * SerializableDemo1 结合SerializableDemo2说明 一个类要想被序列化必须实现Serializable接口 */ public class SerializableDemo1 { public static void main(String[] args) { //Initializes The Object User1 user = new User1(); user.setName("hollis"); user.setAge(23); System.out.println(user); //Write Obj to File ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream("tempFile")); oos.writeObject(user); } catch (IOException e) { e.printStackTrace(); } finally { IOUtils.closeQuietly(oos); } //Read Obj from File File file = new File("tempFile"); ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream(file)); User1 newUser = (User1) ois.readObject(); System.out.println(newUser); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { IOUtils.closeQuietly(ois);try{FileUtils.forceDelete(file);}catch(IOException e){ e.printStackTrace();}}}}//OutPut://User{name='hollis', age=23}//User{name='hollis', age=23}
使用对象流实现序列化
反序列化机制无须通过构造器来初始化Java对象。
如果使用序列化机制向文件中写入多个Java对象,使用反序列化机制恢复对象时必须按实际写入的顺序读取。
当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造器,要么也是可序列化的——否则反序列化时将抛出InvalidClassException异常。如果父类是不可序列化的,只是带有无参数的构造器,则该父类中定义的Field值不会序列化到二进制流中。
对象引用的序列化
当程序序列化一个Teacher对象时,如果该Teacher对象持有一个Person对象的引用,为了在反序列化时可以正常恢复该Teacher对象,程序会顺带将该Person对象也进行序列化,所以Person类也必须是可序列化的,否则Teacher类将不可序列化。
当使用Java序列化机制序列化可变对象时一定要注意,只有第一次调用writeObject()方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream;在后面程序中即使该对象的Field发生了改变,再次调用writeObject()方法输出该对象时,改变后的Field也不会被输出。
Externalizable接口
除了Serializable 之外,java中还提供了另一个序列化接口Externalizable
为了了解Externalizable接口和Serializable接口的区别,先来看代码,我们把上面的代码改成使用Externalizable的形式。
package com.hollischaung.serialization.ExternalizableDemos; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; /** * Created by hollis on 16/2/17. * 实现Externalizable接口 */ public class User1 implements Externalizable { private String name; private int age; 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; } public void writeExternal(ObjectOutput out) throws IOException { } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
package com.hollischaung.serialization.ExternalizableDemos; import java.io.*; /** * Created by hollis on 16/2/17. */ public class ExternalizableDemo1 { //为了便于理解和节省篇幅,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记 //IOException直接抛出 public static void main(String[] args) throws IOException, ClassNotFoundException { //Write Obj to file ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); User1 user = new User1(); user.setName("hollis"); user.setAge(23); oos.writeObject(user); //Read Obj from file File file = new File("tempFile"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); User1 newInstance = (User1) ois.readObject(); //output System.out.println(newInstance); } } //OutPut: //User{name='null', age=0}
通过上面的实例可以发现,对User1类进行序列化及反序列化之后得到的对象的所有属性的值都变成了默认值。也就是说,之前的那个对象的状态并没有被持久化下来。这就是Externalizable接口和Serializable接口的区别:
Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()
与readExternal()
。当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()
与readExternal()
方法。由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空。还有一点值得注意:在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。
按照要求修改之后代码如下:
package com.hollischaung.serialization.ExternalizableDemos; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; /** * Created by hollis on 16/2/17. * 实现Externalizable接口,并实现writeExternal和readExternal方法 */ public class User2 implements Externalizable { private String name; private int age; 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; } public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(age); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); age = in.readInt(); } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
package com.hollischaung.serialization.ExternalizableDemos; import java.io.*; /** * Created by hollis on 16/2/17. */ public class ExternalizableDemo2 { //为了便于理解和节省篇幅,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记 //IOException直接抛出 public static void main(String[] args) throws IOException, ClassNotFoundException { //Write Obj to file ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); User2 user = new User2(); user.setName("hollis"); user.setAge(23); oos.writeObject(user); //Read Obj from file File file = new File("tempFile"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); User2 newInstance = (User2) ois.readObject(); //output System.out.println(newInstance); } } //OutPut: //User{name='hollis', age=23}
这次,就可以把之前的对象状态持久化下来了。
如果User类中没有无参数的构造函数,在运行时会抛出异常:
java.io.InvalidClassException
ObjectOutputStream类和ObjectInputStream类
通过前面的代码片段中我们也能知道,我们一般使用ObjectOutputStream的writeObject
方法把一个对象进行持久化。再使用ObjectInputStream的readObject
从持久化存储中把对象读取出来。
注意:writeObject()方法存储Field的顺序应该和readObject()方法中恢复Field的顺序一致,否则将不能正常恢复该Java对象。
对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的。
这里给出ObjectOutputStream的writeObject的调用栈:
writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject
这里看一下invokeWriteObject:
void invokeWriteObject(Object obj, ObjectOutputStream out) throws IOException, UnsupportedOperationException { if (writeObjectMethod != null) { try { writeObjectMethod.invoke(obj, new Object[]{ out }); } catch (InvocationTargetException ex) { Throwable th = ex.getTargetException(); if (th instanceof IOException) { throw (IOException) th; } else { throwMiscException(th); } } catch (IllegalAccessException ex) { // should not occur, as access checks have been suppressed throw new InternalError(ex); } } else { throw new UnsupportedOperationException(); } }
其中writeObjectMethod.invoke(obj, new Object[]{ out });
是关键,通过反射的方式调用writeObjectMethod方法。官方是这么解释这个writeObjectMethod的:
class-defined writeObject method, or null if none
至此,我们先试着来回答以下的问题:
如果一个类中包含writeObject 和 readObject 方法,那么这两个方法是怎么被调用的?
答:在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法时,会通过反射的方式调用。
那么,不知道有没有人提出这样的疑问:
Serializable明明就是一个空的接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?
Serializable接口的定义:
public interface Serializable { }
其实这个问题也很好回答,我们再回到刚刚ObjectOutputStream的writeObject的调用栈:
writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject
writeObject0方法中有这么一段代码:
if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } }
在进行序列化操作时,会判断要被序列化的类是否是Enum、Array和Serializable类型,如果不是则直接抛出NotSerializableException
。
Transient 关键字
Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。Transient 关键字只能用于修饰Field,不可修饰Java程序中的其他成分。
序列化ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID
)
序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。
对类的哪些修改可能导致该类实例的反序列化失败呢?下面分三种情况来具体讨论:
如果修改类时仅仅修改了方法,则反序列化不受任何影响,类定义无须修改serialVersionUID Field值;
如果修改类时仅仅修改了静态Field或瞬态Field,则反序列化不受任何影响,类定义无须修改serialVersionUID Field值;
如果修改类时修改了非静态Field、非瞬态Field,则可能导致序列化版本不兼容。如果对象流中的对象和新类中包含同名的Field,而Field类型不同,则反序列化失败,类定义应该更新serialVersionUID Field值。如果对象流中的对象比新类中包含更多的Field,则多出的Field值被忽略,序列化版本可以兼容,类定义可以不更新serialVersionUID Field值;如果新类比对象流中的对象包含更多的Field,则序列化版本也可以兼容,类定义可以不更新serialVersionUID Field值;但反序列化得到的新对象中多出的Field值都是null(引用类型Field)或0(基本类型Field)。
防止序列化破坏单例模式
只要在Singleton类中定义readResolve
就可以解决该问题:
package com.hollis; import java.io.Serializable; /** * Created by hollis on 16/2/5. * 使用双重校验锁方式实现单例 */ public class Singleton implements Serializable{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } private Object readResolve() { return singleton; } }
还是运行以下测试类:
package com.hollis; import java.io.*; /** * Created by hollis on 16/2/5. */ public class SerializableDemo1 { //为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记 //Exception直接抛出 public static void main(String[] args) throws IOException, ClassNotFoundException { //Write Obj to file ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); oos.writeObject(Singleton.getSingleton()); //Read Obj from file File file = new File("tempFile"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); Singleton newInstance = (Singleton) ois.readObject(); //判断是否是同一个对象 System.out.println(newInstance == Singleton.getSingleton()); } } //true
本次输出结果为true。主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
所有的单例类、枚举类在实现序列化时都应该提供readResolve()方法,这样才可以保证反序列化的对象依然正常。通常的建议是,对于final类重写readResolve()方法不会有任何问题;否则,重写readResolve()方法时应尽量使用private修饰该方法。
总结
1、如果一个类想被序列化,需要实现Serializable接口。否则将抛出NotSerializableException
异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于Enum、Array和Serializable类型其中的任何一种。
2、在变量声明前加上该关键字,可以阻止该变量被序列化到文件中。
3、在类中增加writeObject 和 readObject 方法可以实现自定义序列化策略。
4、在涉及到序列化的场景时,要格外注意他对单例的破坏。
参考资料
《成神之路-基础篇》Java基础知识——序列化(已完结):http://www.hollischuang.com/archives/1158