本文共 15738 字,大约阅读时间需要 52 分钟。
把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。 对象的序列化主要有两种用途: 1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中; 2) 在网络上传送对象的字节序列。在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。定义一个Person类:
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 | package map; import java.io.Serializable; /** * Created by benjamin on 12/5/15. */ public class Person implements Serializable{ private static final long serialVersionUID = -3751291995104363537L; private int age; private String name; private String sex; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = 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 | package map; import java.io.*; /** * Created by piqiu on 12/8/15. */ public class SerializableTest { private static final String DISK_PATH = "/Users/piqiu1/Person"; public static void main(String[] args) throws IOException, ClassNotFoundException { Person p = new Person(); p.setName("benjamin"); p.setAge(24); p.setSex("man"); serializePerson(p); Person p2 = deserializePerson(); System.out.println("name: " + p2.getName() + ", age: " + p2.getAge() + ", sex: " + p2.getSex()); } private static void serializePerson(Person p) throws IOException { ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(new File(DISK_PATH))); oo.writeObject(p); System.out.println("Person序列化成功"); oo.close(); } private static Person deserializePerson() throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(DISK_PATH))); Person p = (Person)ois.readObject(); System.out.println("Person反序列化成功"); return p; } } |
执行结果:
1 2 3 | Person序列化成功 Person反序列化成功 name: benjamin, age: 24, sex: man |
我们设定的路径下面也生成了序列化的文件。
+Add default serial version ID
这种方式生成的serialVersionUID是1L,例如: +Add generated serial version ID
这种方式生成的serialVersionUID是根据类名,接口名,方法和属性等来生成的,例如: 1 2 3 4 | Exception in thread "main" java.io.InvalidClassException: Customer; 2 local class incompatible: 3 stream classdesc serialVersionUID = -88175599799432325, 4 local class serialVersionUID = -5182532647273106745 |
serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。
类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定serialVersionUID,为它赋予明确的值。显式地定义serialVersionUID有两种用途:
1、 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID; 2、 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。 参考链接:1 2 3 | private void readObjectNoData() throws InvalidObjectException { throw new InvalidObjectException("Stream data required"); } |
下面举出一个例子来更好的理解何时和如何使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Person implements Serializable { private static final long serialVersionUID = -1046907702282365423L; private int age; public Person(){} public int getAge() { return age; } public void setAge(int age) { this.age = age; } } |
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 | public class SerializeTest { private static final String fileUrl = "/Users/piqiu1/Desktop/seralize.txt"; public static void main(String[] args) { write(); // read(); } private static void write() { Person p = new Person(); p.setAge(10); ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream(new File(fileUrl))); oos.writeObject(p); oos.flush(); oos.close(); } catch (IOException e) { e.printStackTrace(); } } private static void read() { ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream(new File(fileUrl))); Person p = (Person)ois.readObject(); System.out.println(p.getAge()); ois.close(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } |
上面是一个简单的序列化和反序列化的例子,现在我们在原来Person类的基础上,继承一个类Animals:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Animals implements Serializable { private static final long serialVersionUID = 2768202150914525915L; private String name; public Animals(){} public String getName() { return name; } public void setName(String name) { this.name = name; } } |
同时让Person继承它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Person extends Animals implements Serializable { private static final long serialVersionUID = -1046907702282365423L; private int age; public Person(){} public int getAge() { return age; } public void setAge(int age) { this.age = age; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 | private static void read() { ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream(new File(fileUrl))); Person p = (Person)ois.readObject(); System.out.println(p.getAge() + " " + p.getName()); ois.close(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } |
上面read的时候输出的p.getName()为空。这里我们就要使用readObjectNoData方法了,在Animals类中加入下面的方法:
1 2 3 | private void readObjectNoData() { this.name = "benjamin"; } |
这样再执行,就能够输出我们给设置的默认值了。
1 2 3 4 5 6 7 8 9 10 | public class Name implements Serializable { private final String lastName; private final String firstName; private final String middleName; ...// } |
从逻辑的角度而言,一个名字包含三个字符串,分别代表姓、名和中间名。Name中的实例域精确地反映了它的逻辑内容。
下面的例子与Name不同,它是另一个极端,该类表示了一个字符串列表:1 2 3 4 5 6 7 8 9 10 11 12 | public class StringList implements Serializable { private int size = 0; private Entry head = null; private static class Entry implements Serializable { String data; Entry next; Entry previous; } ... // } |
当一个对象的物理表示法与它的逻辑数据内容有实质性的区别时,使用默认序列化形式会有以下4个缺点:
1、它使这个类的导出API永远地束缚在该类的内部表示法上。 2、它会消耗过多的空间 3、它会消耗过多的时间:序列化逻辑并不了解对象图的拓扑关系,所以它必须要经过一个昂贵的图遍历(traversal)过程。在上面的例子中,沿着next引用进行遍历是非常简单的。 4、它会引起栈溢出:默认的序列化过程要对对象图执行一次递归遍历,即使对于中等规模的对象图,这样的操作也可能会引起栈溢出。在我的机器上,如果StringList实例包含1258个元素,对它进行序列化就会导致栈溢出。到底多少个元素就会引发栈溢出,这要取决于JVM的具体实现以及Java启动时的命令行参数,(比如Heap Size的-Xms与-Xmx的值)有些实现可能根本不存在这样的问题下面我们使用了writeObject和readObject方法和transient修饰符来改变了这个方法的实现(transient修饰符表明这个实例域将从一个类的默认序列化形式中省略掉)
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 | public class StringList implements Serializable { private transient int size = 0; private transient Entry head = null; private static class Entry { String data; Entry next; Entry previous; } public final void add(String s) {...} private void writeObject(ObjectOutputStream s) throws IOException { s.defaultWriteObject(); s.writeInt(size); //以正确的顺序写出所有元素 for (Entry e = head; e != null; e = e.next) s.writeObject(e.data); } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int numElements = s.readInt(); for (int i = 0; i < numElements; i++) add((String) s.readObject()); } } |
我们还可以利用writeObject和readObject来进行模糊化序列化数据的操作。
假设我们有一个Person类要进行序列化操作,但是里面有一个age字段是敏感数据,毕竟女士忌谈年龄。我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(你可以用更安全的算法代替)
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 70 71 72 73 74 | package Effective; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * Created by piqiu on 2/5/16. */ public class Person extends Animals implements Serializable { private static final long serialVersionUID = -1046907702282365423L; private String firstName; private String lastName; private int age; private Person spouse; public Person(String fn, String ln, int a) { this.firstName = fn; this.lastName = ln; this.age = a; } private void writeObject(ObjectOutputStream stream) throws IOException { // 加密数据 age = age << 2; stream.defaultWriteObject(); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); // 解密数据 age = age << 2; } @Override public String toString() { return "[Person: firstName=" + firstName + " lastName=" + lastName + " age=" + age + " spouse=" + (spouse != null ? spouse.getFirstName() : "[null]") + "]"; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Person getSpouse() { return spouse; } public void setSpouse(Person spouse) { this.spouse = spouse; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | package Effective; import java.io.Serializable; /** * Created by piqiu on 2/5/16. */ public class MySingletion implements Serializable { private static final long serialVersionUID = -2786296717146940199L; private MySingletion(){} private static final MySingletion instance = new MySingletion(); public static MySingletion getInstance() { return instance; } private Object readResolve() { return instance; } } |
很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。
如果首要问题是序列化,那么最好指定一个 flyweight 或代理放在流中。为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。 writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。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 | class PersonProxy implements java.io.Serializable { public PersonProxy(Person orig) { data = orig.getFirstName() + "," + orig.getLastName() + "," + orig.getAge(); if (orig.getSpouse() != null) { Person spouse = orig.getSpouse(); data = data + "," + spouse.getFirstName() + "," + spouse.getLastName() + "," + spouse.getAge(); } } public String data; private Object readResolve() throws java.io.ObjectStreamException { String[] pieces = data.split(","); Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2])); if (pieces.length > 3) { result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt (pieces[5]))); result.getSpouse().setSpouse(result); } return result; } } public class Person implements java.io.Serializable { public Person(String fn, String ln, int a) { this.firstName = fn; this.lastName = ln; this.age = a; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public Person getSpouse() { return spouse; } private Object writeReplace() throws java.io.ObjectStreamException { return new PersonProxy(this); } public void setFirstName(String value) { firstName = value; } public void setLastName(String value) { lastName = value; } public void setAge(int value) { age = value; } public void setSpouse(Person value) { spouse = value; } public String toString() { return "[Person: firstName=" + firstName + " lastName=" + lastName + " age=" + age + " spouse=" + spouse.getFirstName() + "]"; } private String firstName; private String lastName; private int age; private Person spouse; } |
注意,PersonProxy 必须跟踪 Person 的所有数据。这通常意味着代理需要是 Person 的一个内部类,以便能访问 private 字段。有时候,代理还需要追踪其他对象引用并手动序列化它们,例如 Person 的 spouse。
这种技巧是少数几种不需要读/写平衡的技巧之一。例如,一个类被重构成另一种类型后的版本可以提供一个 readResolve 方法,以便静默地将被序列化的对象转换成新类型。类似地,它可以采用 writeReplace 方法将旧类序列化成新版本。转载地址:http://ynuix.baihongyu.com/