【Java】详解java对象的序列化
目录结构:
1.序列化的含义和意义
序列化机制允许将实现序列化的Java对象转化为字节序列,这些字节序列可以保存到磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而单独存在。如果需要让某个类支持序列化机制,那么必须实现Serializable或Externalizable接口。在Java库中,已经有许多类实现了Serializable接口,Serializable是一个标记接口,该接口无需实现任何方法,它只是表明该类是可序列化的。所有可能在网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常,比如RMI(Remote Method Invoke,远程方法调用)过程中的参数和返回值;所有需要保持到磁盘里的对象的类都必须是可序列化的。
使用java序列化的时候需要注意:
a.对象的类名,实例变量都会被序列化;方法,类变量,transient实例变量不会被序列化。
b.实现Serializable接口的类如果需要让某个实例变量不会序列化,则可在实例变量前加transient修饰符,而不是static关键字。虽然static关键字可以达到效果,但是static关键字不是这样用的。
c.保证序列化对象的实例变量也是可序列化的,否则需要使用transient关键字来修饰该变量。
d.反序列化对象时,必须有序列化对象的class文件。
e.通过网络、文件来传输序列化的对象时候,必须按照实际写入的顺序读取。
2.使用对象流实现序列化
在上面我们已经了解到,如果需要将某个对象保持到磁盘或通过网络传输,那么该类该类应该实现Serializable接口或Externalizable接口。
为了演示,首先定义一个Person类:
public class Person implements Serializable{ public String name; public int age; public Person(){ System.out.println("Person的无参构造器"); } public Person(String name,int age) { this.name=name; this.age=age; } public String toString(){ return "name:"+name+",age:"+age; } @Override protected Object clone() throws CloneNotSupportedException { System.out.println("Person的clone方法里"); return super.clone(); } }
下面的代码将Person对象保存到磁盘person.out文件中:
public class WriteObject { public static void main(String[] args) throws Exception{ Person person=new Person("孙悟空",500); FileOutputStream fos=new FileOutputStream(new File("person.out")); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(person); } }
接下来的代码将从person.out文件中,反序列化一个Person对象:
public class ReadObject { public static void main(String[] args) throws Exception{ FileInputStream fis=new FileInputStream(new File("person.out")); ObjectInputStream ois=new ObjectInputStream(fis); Object obj=ois.readObject(); System.out.println(obj.getClass().getSimpleName()); } }
可以看见控制台打印了
Person
观察上面反序列化和Person的代码。可以看出,通过实现Serializable接口序列化后,再反序列化重新构建对象是没有经过Person类的无参构造器和clone方法的。这里可以暂时理解为一种特殊创建对象的方式。
3.对象引用的序列化
在上面的案例中,我们定义的Person类有两个,name和age,分别为String和int类型。通过观察String可以发现String实现了Serializable接口。其实只要一个类里面有引用类型,那么这个引用类型也必须可序列化,否则拥有该类型成员变量的类也不能序列化。
例如,在下面定义了一个Teacher类,并且持有List<Person>的引用:
public class Teacher implements Serializable{ public String name; public List<Person> students=null; public Teacher(String name,List<Person> students) { this.name=name; this.students=students; } }
上面的类成员变量List<Person> students,其中List、Person和Person类中的引用类型成员变量都实现了Serializable接口,倘若有一个没有实现Serializable接口,那么这个Teacher都不能被成功序列化。
我们创建了三个对象students,teacher,teacher2:
List<Person> students=new ArrayList<Person>(); students.add(new Person("孙悟空",500)); students.add(new Person("猪八戒",400)); students.add(new Person("沙僧",300)); Teacher teacher1=new Teacher("唐僧",students);//引用List<Person> Teacher teacher2=new Teacher("玄奘",students);//引用List<Person>
在这里需要注意的是,我们依次把这三个对象序列化到本地文件中,那么是否会得到三个不同的List<Person>对象呢,倘若是,那么teacher1和teacher2引用的对象就不是同一个对象了,显然违背了java序列化机制的初衷了。在java中,进行了特殊处理,同一个对象只会被序列化一次。
我们使用如下代码来进行验证:
public class WriteReadTest { public static void main(String[] args) throws Exception { List<Person> students=new ArrayList<Person>(); students.add(new Person("孙悟空",500)); students.add(new Person("猪八戒",400)); students.add(new Person("沙僧",300)); Teacher teacher1=new Teacher("唐僧",students); Teacher teacher2=new Teacher("玄奘",students); //开始序列化对象 FileOutputStream fos=new FileOutputStream(new File("person.out")); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(students); oos.writeObject(teacher1); oos.writeObject(teacher2); //开始反序列化对象 FileInputStream fis=new FileInputStream(new File("person.out")); ObjectInputStream ois=new ObjectInputStream(fis); List<Person> s=(List<Person>)ois.readObject(); Teacher t1=(Teacher)ois.readObject(); Teacher t2=(Teacher)ois.readObject(); System.out.println(s==t1.students);//true System.out.println(s==t2.students);//true System.out.println(t1==t2);//false } }
可以看出反序列化出来的三个List<Person>对象都是同一个对象。
java的序列化机制采用了如下的算法:
a.所有保存到磁盘中的对象都有一个序列化编号
b.当程序试图序列化一个对象时,程序将检查该对象是否被序列化过,只有该对象从未被序列化(在本次虚拟机中)过,系统才会将该对象转化为字节序列输出。
c.如果某个对象已经被序列化过了,程序只是输出一个序列化编号,而不是重新序列化该对象。
通过上面的算法,我们可以得出一个结论,倘若有一个对象被多次序列化,那么只有第一次会成功被序列化,其它几次只是输出序列化编号而已,例如:
可以用如下图来进行进一步理解:
4.自定义序列化
实现自定义序列化,可以通过实现Serializable或Externalizable接口。
4.1 采用实现Serializable接口实现序列化
通过实现Serializable接口来实现序列化是比较常用的,Serializable是一个标记性接口,实现该接口不需要做多余的额外工作。
接下来会介绍一些常见的操作和注意事项,
在一些特殊情况下,一个类里包含的实例变量是敏感信息,不希望被序列化到本地,那么可以在此变量上使用transient关键字。
例如:
public class Person implements Serializable{ public transient String name;//transient表明 不允许被序列化到本地 public int age; public Person(String name,int age) { this.name=name; this.age=age; } }
测试:
Person p1=new Person("孙悟空",500); //开始序列化对象 FileOutputStream fos=new FileOutputStream(new File("person.out")); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(new Person("孙悟空",500)); //开始反序列化对象 FileInputStream fis=new FileInputStream(new File("person.out")); ObjectInputStream ois=new ObjectInputStream(fis); Person person1=(Person)ois.readObject(); System.out.println("name:"+person1.name+",age:"+person1.age);//name:null,age:500
可以在需要被序列化的列中,定义writeReplace()方法。JVM在进行序列化时候,如果未定义该方法,则不会进行序列化,如果定义了该方法,那么该方法在writeObject之后调用。
writeObject方法的完整签名格式为:
Object writeReplace() throws ObjectStreamException
writeReplace在writeObject之后调用,一旦定义了writeReplace方法,那么由writeObject序列化的对象,会完全丢弃,程序被序列化的对象是writeReplace所返回的。
通过这个特点,我们可以把被序列化的对象,替换为我们想要的任意类型:
例如:
public class Person implements Serializable{ public String name; public int age; public Person(String name,int age) { this.name=name; this.age=age; } Object writeReplace() throws ObjectStreamException { List<Object> list=new ArrayList<Object>(); list.add(name); list.add(age); return list;//返回了一个List<Object>类型的数据 } }
我们看到writeReplace方法返回了一个List<Object>类型的数据。
public static void main(String[] args) throws Exception { Person p1=new Person("孙悟空",500); //开始序列化对象 FileOutputStream fos=new FileOutputStream(new File("person.out")); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(p1); //开始反序列化对象 FileInputStream fis=new FileInputStream(new File("person.out")); ObjectInputStream ois=new ObjectInputStream(fis); List<Object> p=(List<Object>)ois.readObject(); for(Object obj : p) { System.out.println(obj); } }
和writeReplace方法相对的就是readResolve方法,readResolve方法会在readObject()方法之后调用,
readResolve方法的完整签名:
Object readResolve() throws ObjectStreamException
由于readResolve方法会在readObject之后立即调用,该方法的返回值会替代原来反序列化的对象,而原来readObject反序列化的对象将会被立即抛弃。
readResolve方法在序列化单例类、枚举类时尤其有用。如果使用java5提供的枚举,当然没问题,但如果在java5以前,那么在序列化时,就需要注意了
public class Orientation implements Serializable{ public static final Orientation HORIZONTAL=new Orientation(1); public static final Orientation VERTICAL=new Orientation(2); private int value; private Orientation(int value){ this.value=value; } }
这样的代码在java5以前经常用来表示枚举,如果使用如下的代码进行序列化:
Orientation o1=Orientation.HORIZONTAL; //开始序列化对象 FileOutputStream fos=new FileOutputStream(new File("person.out")); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(o1); //开始反序列化对象 FileInputStream fis=new FileInputStream(new File("person.out")); ObjectInputStream ois=new ObjectInputStream(fis); Orientation o=(Orientation)ois.readObject(); System.out.println(o==o1);//false
会发现结果为false,这显然不是我们想要的。这是因为反序列化的对象时重新构建的对象,我们可以定义readResolve方法来解决这个问题:
public class Orientation implements Serializable{ public static final Orientation HORIZONTAL=new Orientation(1); public static final Orientation VERTICAL=new Orientation(2); private int value; private Orientation(int value){ this.value=value; } Object readResolve() throws ObjectStreamException { if(value==1){ return HORIZONTAL; }else if(value==2) { return VERTICAL; }else{ return null; } } }
这样一来,被序列化前后的对象就相等了。这样之所以,是因为序列化不包括静态变量,由于readObject后会立即调用readResolve方法,我们又在readResolve中返回了一个类变量,所以前后得到的是同一个对象。
接下来附张图片,表示writeObject,writeReplace,readObject,readResolve方法间的调用前后顺序:
4.2采用实现Externalizable接口实现序列化
采用实现Externalizable的方式和实现Serializable的方式具有相同的效果。在上面介绍Serializable里的常用操作和方法在Externalizable接口里也同样适用,这里就不一一介绍那些操作。
采用实现Externalizable接口的方法,必须重新Externalizable接口里的两个抽象方法writeExternal和readExternal方法。
public class Person implements Externalizable{ public String name; public int age; public Person(){ System.out.println("公共无参构造器"); } public Person(String name,int age) { this.name=name; this.age=age; } /** * 在序列化的时候调用 */ @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeInt(age); } /** * 在反序列化的时候调用 */ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { this.name=(String)in.readObject(); this.age=in.readInt(); } }
测试代码为:
public static void main(String[] args) throws Exception { Person p=new Person("孙悟空",500); //开始序列化对象 FileOutputStream fos=new FileOutputStream(new File("person.out")); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(p); //开始反序列化对象 FileInputStream fis=new FileInputStream(new File("person.out")); ObjectInputStream ois=new ObjectInputStream(fis); Person p2=(Person)ois.readObject(); System.out.println("name:"+p2.name+",age:"+p2.age); }
打印结果为:
公共无参构造器
name:孙悟空,age:500
我们可以看到执行了Person的公共无参构造器,这是使用Externalizable和Serializable的不同点;使用Externalizable来反序列化时,是调用反序列化的类的公共无参构造器,然后在readExternal方法中对成员变量赋值,而Serializable是不会调用任何构造器的。
5序列化的版本问题
通过前面的介绍,我们知道了反序列化java对象必须提供该对象的class文件,现在的问题是,顺着项目的升级,系统的class文件也会升级,java如何保证两个class文件的兼容性?
java序列化机制允许为序列化类提供一个private static final 的serialVersionUID值,该类变量的值用于表示该java类的序列化版本,也就是一个类升级后,只要它的serialVersionUID的值不变,序列化机制也会把他们当做同一个序列化版本。
public class Test{ private static final long serialVersionUID=512L; }
为了在反序列化时确保序列化版本的兼容性,最好在每个要序列化的的类中加入private static final long seriaVersionUID值的类变量,具体数值自己定义。这样,计时在某个类序列化之后,它所对应的类被修改了,该对象也依然可以被正确的反序列化。
可以通过JDK的bin目录下的serialver.exe工具来获得该类的serialVersionUID类变量的值。
serialver Person