序列化和反序列化的概念
把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。
对象的序列化主要有两种用途:
1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
2) 在网络上传送对象的字节序列。
在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
JDK类库中的序列化API
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
只有实现了Serializable和Externalizable接口的类的对象才能被序列化。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以 采用默认的序列化方式 。
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。
对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。
对象序列化和反序列范例:
定义一个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
|
我们设定的路径下面也生成了序列化的文件。
serialVersionUID的作用
serialVersionUID: 字面意思上是序列化的版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量
private static final long serialVersionUID
一般如果在eclipse或者myeclipse中不加版本号会出现警告。
采用
+Add default serial version ID
这种方式生成的serialVersionUID是1L,例如:
private static final long serialVersionUID = 1L;
采用
+Add generated serial version ID
这种方式生成的serialVersionUID是根据类名,接口名,方法和属性等来生成的,例如:
private static final long serialVersionUID = 4603642343377807741L;
添加了之后就不会出现警告了。
在IDEA编辑器中可能会有的人不加版本号也不会警告,这是因为配置的问题。
打开Preferences -> 搜索serialVersionUID -> 勾选serializable class without serialVersionUID -> Apply
这样就会出现提示了。
那么serialVersionUID(序列化版本号)到底有什么用呢?
如果不加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的取值
serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。
类的serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID,也有可能相同。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显示的定serialVersionUID,为它赋予明确的值。
显式地定义serialVersionUID有两种用途:
1、 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
2、 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。
参考链接:http://www.cnblogs.com/xdp-gacl/p/3777987.html
readObjectNoData的使用
实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。
如果你实现了一个带有实例域的类,它是可序列化和可扩展的,你就应该担心这样一条告诫。如果类有一些约束条件,当类的实例域被初始化成它们的默认值(整数类型为0,boolean为false,对象引用类型为null)时,就会违背这些约束条件,这时候你就必须给这个类添加这个readObjectNoData方法:
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();
}
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; } }
|
writeReplace和readResolve的使用
序列化非常强大,我们可以通过流把对象保存在磁盘上,再从磁盘上读取转为对象,但是如果单例的对象这么转换过后可就不是单例了,为了防止这种情况发生,我们可以在单例中加入readResolve这个方法来保证单例的可靠性。
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 方法将旧类序列化成新版本。