1,定义
JAVA序列化就是把JAVA对象转化为字节序列。JAVA对象:在内存中,当程序运行结束,对象就被清理;字节序列:可在输入输出流中传送,可保存到磁盘中。
本文讨论实现了java.io.Serializable接口中的默认方式下的序列化
2,JDK中涉及的序列化API
java.io.Serializable:标记接口,实现该接口的类可序列化。
java.io.ObjectOutputStream java.io.ObjectInputStream 连接字节序列与JAVA对象的输入输出流。
3,对象序列化的步骤及反序列化的步骤
序列化步骤:
①创建一个对象输出流,如:
OutputStream out = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
②通过对象输出流的writeObject()方法将对象写入到输出流中,如:oos.writeObject(new Date());
反序列化步骤:
①创建一个对象输入流,如:
InputStream in = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
②通过对象输入流的readObject()方法读取对象,如:Date obj = (Date)in.readObject();</span>
注意:为了能正确地读取数据,须保证向对象输出流写对象的顺序与从对象输入流读对象的顺序一致。反序列化时,不会调用类的构造方法但是会调用类的静态初始化代码块。
4,transient关键字
在默认的序列化方式下,若实现了Serializable接口的类中的实例被transient关键字修饰,则不会对该实例进行序列化。此外需要注意,类中的静态变量(非final 修饰)也不会序列化。
当序列化该对象时,若该对象的类里面定义了上面二个方法,就会调用这二个方法进行序列化了。因此,可以先在自定义的writeObject()方法里进行一些其他处理操作,然后再将对象写入到输出流中进行序列化。如:
注意这两个方法是用private 修饰的。这意味着子类无法复用父类的序列化方法。若要子类复用父类的序列化方法,可参考:
而实际上,若要构造一个原链表,只需要知道原链表的大小(size)和每个结点保存的数据即可。
因此,可以按如下方式改进序列化,以提高程序的性能:
从自定义序列化的方法中可以看出,序列化只是保存每个结点的值,而不保存结点之间的关系。
为什么是 从SerialVersionID角度上来看,A的这两个版本是序列化兼容的呢?
因为,序列化兼容性不仅取决于SerialVersionID,而取决于类的不同版本之间的实现细节及序列化操作的细节。
8,参考资料:
JAVA自定义序列化
http://www.ibm.com/developerworks/cn/java/j-5things1/
public class Customer implements Serializable{
private static int count;//不会被序列化
private static final int MAX_COUNT = 100;
private String name;
private transient String password;//不会被序列化
5,序列化对象的图---当一个类中引用了另一个类中的对象时
在默认方式下,当序列化对象A时,若A持有B对象的引用,那么B对象(当然,B需要implements Serializable)也会被序列化。
6,自定义序列化
默认情况下序列化步骤见上面第3点。但当因为安全性或者性能方面的原因需要自定义序列化时,需要在可序列化的类中定义如下二个方法:
private void writeObject(ObjectOutputStream out) throws IOException {
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
当序列化该对象时,若该对象的类里面定义了上面二个方法,就会调用这二个方法进行序列化了。因此,可以先在自定义的writeObject()方法里进行一些其他处理操作,然后再将对象写入到输出流中进行序列化。如:
private void writeObject(ObjectOutputStream outputStream)throws IOException{
outputStream.defaultWriteObject();//先使用默认的序列化方式
outputStream.writeObject(encrypt(password.getBytes()));//对密码进行加密操作后,再写入输出流
}
注意这两个方法是用private 修饰的。这意味着子类无法复用父类的序列化方法。若要子类复用父类的序列化方法,可参考:
java.io.Externalizable
自定义序列化提高性能的例子
比如需要序列化一个双向链表。当链表中包含的结点个数很多时,由于需要保存大量的结点对象之间的引用关系,可能会因内存不够而出现StackOverflow Exception。
若只序列化链表的长度及链表中每个结点保存的结点的内容,则可以提高程序的性能。具体代码如下:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/*
* StringList 是一个双向链表
*/
public class StringList implements Serializable{
private int size = 0;//链表中结点的个数
private Node head = null;//链表头指针
private Node end = null;//链表尾指针
private static class Node implements Serializable{
String data;//结点数据
Node next;//指向下一个结点
Node previous;//指向前驱结点
}
//向链表中添加新结点--在链表的尾部添加新结点
public void add(String data){
Node node = new Node();//创建一个新结点
node.data = data;
node.next = null;//由于是在尾部添加结点,故新创建的结点的next域为null
node.previous = end;//由于end是链表的尾指针即总是指向链表的最后一个结点,故新添加的结点的前驱指向 end
if(end != null)//说明链表中已经存在其他结点了(end==null && head == null 表明双向链表为空)
end.next = node;//将链表中的最后一个结点的next指向新增加的结点
size++;
end = node;//将尾指针指向新结点(因为新结点总是插入在末尾)
if(size == 1)
head = end;//链表中只有一个结点时,头指针和尾指针都指向同一个结点
}
//index : [0, size-1]
public String get(int index){
if(index >= size)
return null;
Node node = head;
for(int i = 1; i <= index; i++){
node = node.next;
}
return node.data;
}
public int size(){
return size;
}
public static void main(String[] args){
StringList list = new StringList();
for(int i = 0; i < 2500; i++)
list.add("hello" + i);
ByteArrayOutputStream buf = null;
ObjectInputStream ois = null;
buf = new ByteArrayOutputStream();
ObjectOutputStream oos = null;
try{
oos = new ObjectOutputStream(buf);
oos.writeObject(list);
ois = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
list = (StringList)ois.readObject();
}catch(IOException e){
e.printStackTrace();
}catch(ClassNotFoundException ex){
ex.printStackTrace();
}
System.out.println("after serialize: " + list.size());
}
}
当在main方法中add的结点个数太多时,会抛出StackOverflow Exception。因为序列化整个链表对象时,会保存链表中各个结点对象之间的关系和状态。而实际上,若要构造一个原链表,只需要知道原链表的大小(size)和每个结点保存的数据即可。
因此,可以按如下方式改进序列化,以提高程序的性能:
public class StringList implements Serializable{
private transient int size = 0;//链表中结点的个数
private transient Node head = null;//链表头指针
private transient Node end = null;//链表尾指针
将链表的属性用transient修饰,默认的序列化方式将忽略它们。private void writeObject(ObjectOutputStream outputStream)throws IOException{
outputStream.defaultWriteObject();//先进行默认序列化
outputStream.writeInt(size);//将链表的大小序列化
for(Node node = head; node != null; node = node.next)
outputStream.writeObject(node.data);//只序列化链表中每个结点的值
}
private void readObject(ObjectInputStream inputStream)throws IOException, ClassNotFoundException{
inputStream.defaultReadObject();
int count = inputStream.readInt();
for(int i = 0; i < count; i++)
add((String)inputStream.readObject());
}
在StringList类中增加两个方法,自定义实现序列化。从自定义序列化的方法中可以看出,序列化只是保存每个结点的值,而不保存结点之间的关系。
7,SerialVersionID的作用
SerialVersionID用来标记序列化类之间的兼容性。比如,A实现了Serializable接口并定义了一个SerialVersionID,随后对A进行了某些改变,若改变后的A SerialVersionID仍与原来的一致,则从SerialVersionID角度上来看,A的这两个版本是序列化兼容的。为什么是 从SerialVersionID角度上来看,A的这两个版本是序列化兼容的呢?
因为,序列化兼容性不仅取决于SerialVersionID,而取决于类的不同版本之间的实现细节及序列化操作的细节。
8,参考资料:
JAVA自定义序列化
http://www.ibm.com/developerworks/cn/java/j-5things1/