序列化和反序列化 [TOC]
概念 把对象转换为字节序列的过程称为对象的序列化。 把字节序列恢复为对象的过程称为对象的反序列化
用途 对象的序列化主要有两种用途: 1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中; 2) 在网络上传送对象的字节序列。
Java中使用方法 java.io.ObjectOutputStream
代表对象输出流,它的writeObject(Object obj)
方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream
代表对象输入流,它的readObject()
方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
只有实现了Serializable
接口可以被序列化,但序列化只能采用默认的序列化。而实现了Externalizable
接口的类也可以实现序列化,因为它继承自Serializable
接口,且可以完全由自身来控制序列化的行为。
一个简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import java.io.*;public class Main implements Serializable { public static void main (String[] args) { System.out.println("Hello world" ); SerializeDemo(); DeserializeDemo(); } private static void SerializeDemo () { try { Test test = new Test("John" , 15 ); ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("/root/Desktop/test.txt" ))); oo.writeObject(test); System.out.println("序列化结束" ); oo.close(); }catch (FileNotFoundException e){ System.out.println("无法读写文件" ); }catch (IOException e){ System.out.println("IO异常" ); } } private static void DeserializeDemo () { try { ObjectInputStream oi = new ObjectInputStream(new FileInputStream(new File("/root/Desktop/test.txt" ))); Test test = (Test)oi.readObject(); System.out.println("反序列化成功" ); System.out.println(test.toString()); }catch (Exception e){ e.printStackTrace(); } } } class Test implements Serializable { private static final long serialVersionUID = 1L ; private int age; private String name; private String sex; public Test (String name, int age) { this .age = age; this .name = name; } @Override public String toString () { return "Test{" + "age=" + age + ", name='" + name + '\'' + '}' ; } }
serialVersionUID的作用 serialVersionUID字面意思是序列化的版本号,凡是实现了Serializable接口的类都有一个表示序列化版本标识符的静态变量。
如果没有serialVersionUID,也是可以编译通过的,对正常的序列化反序列化是没有影响的,至于有影响的情况,看下面解释。
serialVersionUID有两种生成方式
采用默认的serivalVersionUID也就是默认的1L,private static final long serialVersionUID = 1L;
通过类名、接口名、方法和属性等来生成,如 private static final long serialVersionUID = 4603642343377807741L;
serialVersionUID的作用
问题 :假如有这么一个类,序列化程二进制数据保存在磁盘中,但是该类的结构发生了变化,如添加了一个属性,此时可以再将磁盘中的二进制数据反序列成这个类吗?
答 :
先去除servialVersionUID
属性,然后序列化保存 在Test
类中添加属性sex
,然后直接反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.dcLunatic;import java.io.*;public class Main implements Serializable { public static void main (String[] args) { System.out.println("Hello world" ); DeserializeDemo(); } private static void SerializeDemo () { try { Test test = new Test("John" , 15 ); ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("/root/Desktop/test.txt" ))); oo.writeObject(test); System.out.println("序列化结束" ); oo.close(); }catch (FileNotFoundException e){ System.out.println("无法读写文件" ); }catch (IOException e){ System.out.println("IO异常" ); } } private static void DeserializeDemo () { try { ObjectInputStream oi = new ObjectInputStream(new FileInputStream(new File("/root/Desktop/test.txt" ))); Test test = (Test)oi.readObject(); System.out.println("反序列化成功" ); System.out.println(test.toString()); }catch (Exception e){ e.printStackTrace(); } } } class Test implements Serializable { private int age; private String name; private String sex; public Test (String name, int age) { this .age = age; this .name = name; } @Override public String toString () { return "Test{" + "age=" + age + ", name='" + name + '\'' + '}' ; } }
此时,运行后程序就会抛出异常
1 2 3 4 5 6 7 8 9 10 Hello world java.io.InvalidClassException: com.dcLunatic.Test; local class incompatible: stream classdesc serialVersionUID = -3765794941312665207, local class serialVersionUID = -2796904131256425943 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1883) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1749) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2040) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1571) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at com.dcLunatic.Main.DeserializeDemo(Main.java:30) at com.dcLunatic.Main.main(Main.java:11)
因为在Test
类中缺少了serialVersionUID,而在编译时,该值会由编译器自动生成,因为前后两次的serialVersionUID不一致,所以导致反序列化失败(序列化版本不一致),此时,如果显式的指定serialVersionUID的值,就不会出现这个问题,反序列化时对后来新增的属性sex就不会有任何赋值修改。
就算类不会发生改动,但在不同的编译器中,自动生成的serialVersionUID的值可能也会存在差异的。
serialVersionUID的作用还是很大的,比如在那个face
transient关键字 在序列化的过程中,如果有一个特定的属性字段比较敏感,不想被序列化,可以考虑使用transient关键字关闭序列化处理。
这里有一个更加常用的方法,父类不实现Serializable接口,但是子类实现,将不想要序列化的放在父类中,子类中放想要序列化的内容即可。
如果子类实现Serializable接口而父类未实现时,父类不会被序列化。
如果父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口。
原因 :跟子类父类的内存分配有关,点这里 或者是这里 阅读相关博文。
自定义writeObject方法和readObject方法 实现Serializable接口的类序列化的话,是使用默认的序列化方式。但我们可以使用writeObject
和readObject
方法来实现对序列化的更多控制。
在序列化过程中,虚拟机会试图调用对象类里的writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject
方法以及调用ObjectInputStream 的 defaultReadObject
方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class Test implements Serializable { private static final long serialVersionUID = 1L ; private String password = "pass" ; public String getPassword () { return password; } public void setPassword (String password) { this .password = password; } private void writeObject (ObjectOutputStream out) { try { PutField putFields = out.putFields(); System.out.println("原密码:" + password); password = "encryption" ; putFields.put("password" , password); System.out.println("加密后的密码" + password); out.writeFields(); } catch (IOException e) { e.printStackTrace(); } } private void readObject (ObjectInputStream in) { try { GetField readFields = in.readFields(); Object object = readFields.get("password" , "" ); System.out.println("要解密的字符串:" + object.toString()); password = "pass" ; } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static void main (String[] args) { try { ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("result.obj" )); out.writeObject(new Test()); out.close(); ObjectInputStream oin = new ObjectInputStream(new FileInputStream( "result.obj" )); Test t = (Test) oin.readObject(); System.out.println("解密后的字符串:" + t.getPassword()); oin.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
defaultWriteObject方法和defaultReadObject方法不会检查serialVersionUID的值是否一致
Externalizable Java默认的序列化机制非常简单,而且序列化后的对象不需要再次调用构造器重新生成,但是在实际中,我们可以会希望对象的某一部分不需要被序列化,或者说一个对象被还原之后,其内部的某些子对象需要重新创建,从而不必将该子对象序列化。在这些情况下,我们可以考虑实现Externalizable接口从而代替Serializable接口来对序列化过程进行控制。
Externalizable接口extends Serializable接口,而且在其基础上增加了两个方法:writeExternal()和readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊的操作。
一个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package com.dcLunatic;import java.io.*;public class ExternDemo { public static void main (String[] args) { System.out.println("Hello world" ); SerializeDemo(); DeserializeDemo(); } private static void SerializeDemo () { try { Test1 test = new Test1("John" , 15 ); ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File("/root/Desktop/test.txt" ))); test.writeExternal(oo); System.out.println("序列化结束" ); oo.close(); }catch (FileNotFoundException e){ System.out.println("无法读写文件" ); }catch (IOException e){ System.out.println("IO异常" ); } } private static void DeserializeDemo () { try { ObjectInputStream oi = new ObjectInputStream(new FileInputStream(new File("/root/Desktop/test.txt" ))); Test1 test = new Test1(); test.readExternal(oi); System.out.println("反序列化成功" ); System.out.println(test.toString()); }catch (Exception e){ e.printStackTrace(); } } } class Test1 implements Externalizable { private String name; private int age; private static final long serialVersionUID = 1L ; public Test1 (String name, int age) { this .name = name; this .age = age; } public Test1 () { } @Override public String toString () { return "Test1{" + "name='" + name + '\'' + ", age=" + age + '}' ; } @Override public void writeExternal (ObjectOutput objectOutput) throws IOException { System.out.println("调用writeExternal方法" ); objectOutput.writeObject(name); objectOutput.writeInt(age); } @Override public void readExternal (ObjectInput objectInput) throws IOException, ClassNotFoundException { System.out.println("调用readExternal方法" ); name = (String)objectInput.readObject(); age = objectInput.readInt(); } }
注意事项
序列化时,只对对象的状态进行保存,而不管对象的方法;
记住,状态状态状态!
当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;
并非所有的对象都可以序列化,,至于为什么不可以,有很多原因了,比如:
安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行rmi传输 等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的。
资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分 配,而且,也是没有必要这样实现