对象序列化的目标是将对象保存到磁盘中,或容许在网络中直接传输对象。对象序列化机制容许把内存中的Java对象转换成平台无关的二进制流,从而容许把这种二进制流持久地保存在磁盘上,经过网络将这种二进制流传输到另外一个网络节点。其余程序一旦得到了这种二进制流,均可以将这种二进制流恢复成原来的Java对象。 java
对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,经过对象序列化,能够把对象保存为字节数组,这些字节数组能够保存在磁盘上,或经过网络传输。在有须要的时候将这个字节数组经过反序列化的方式再转换成对象。对象序列化能够很容易的在JVM中的活动对象和字节数组(流)之间进行转换。apache
在Java中,对象的序列化与反序列化被普遍应用到RMI(远程方法调用)及网络传输中。一般建议:程序建立的每一个JavaBean类都实现Serializable。数组
Java为了方便开发人员将Java对象进行序列化及反序列化提供了一套方便的API来支持。其中包括如下接口和类:网络
java.io.Serializableide
java.io.Externalizable函数
ObjectOutput工具
ObjectInput测试
ObjectOutputStreamui
ObjectInputStreamthis
类经过实现 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也不会被输出。
除了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的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 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。Transient 关键字只能用于修饰Field,不可修饰Java程序中的其余成分。
虚拟机是否容许反序列化,不只取决于类路径和功能代码是否一致,一个很是重要的一点是两个类的序列化 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修饰该方法。
一、若是一个类想被序列化,须要实现Serializable接口。不然将抛出NotSerializableException
异常,这是由于,在序列化操做过程当中会对类型进行检查,要求被序列化的类必须属于Enum、Array和Serializable类型其中的任何一种。
二、在变量声明前加上该关键字,能够阻止该变量被序列化到文件中。
三、在类中增长writeObject 和 readObject 方法能够实现自定义序列化策略。
四、在涉及到序列化的场景时,要格外注意他对单例的破坏。
《成神之路-基础篇》Java基础知识——序列化(已完结):http://www.hollischuang.com/archives/1158