JAVA 序列化就是将 JAVA 对象以一种形式保持,比如存放到硬盘,或是用于传输。反序列化是序列化的一个逆过程。
JAVA 规定被序列化的对象必须实现 java.io.Serializable 这个接口,而我们分析的 ArrayList 同样实现了该接口。
通过对 ArrayList 源码的分析,可以知道 ArrayList 的数据存储依赖于 elementData 数组,它的声明为:
transient Object[] elementData;
注意 transient 修饰着 elementData 这个数组。
我们都知道一个对象只要实现了 Serializable 接口,这个对象就可以被序列化,java 的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了 Serializable 接口,这个类的所有属性都会自动序列化。
然而在实际开发过程中,我们常常会遇到这样的问题,类的有些属性需要序列化,有些属性不需要序列化,打个比方,例如用户的敏感信息(如密码,银行卡号等),不希望在网络(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上 transient 关键字。
总之,java 的 transient 关键字为我们提供了便利,你只需要实现 Serializable 接口,将不需要序列化的属性前添加关键字 transient,对象序列化的时候,这个属性就不会序列化到指定的目的地中。
既然 elementData 被 transient 修饰,按理来说,它不能被序列化的,那么 ArrayList 又是如何解决序列化这个问题的呢?
类通过实现 java.io.Serializable 接口可以启用其序列化功能。要序列化一个对象,必须与一定的对象输出/输入流联系起来,通过对象输出流将对象状态保存下来,再通过对象输入流将对象状态恢复。
在序列化和反序列化过程中需要特殊处理的类,必须严格遵从下面的写法(私有,void返回值类型,函数名,参数):
private void writeObject(java.io.ObjectOutputStream out)
private void readObject(java.io.ObjectInputStream in)
a) 写入
b) 读取
举例说明:
public class Box implements Serializable {
private static final long serialVersionUID = -3450064362986273896L;
private int width;
private int height;
public static void main(String[] args) {
Box myBox = new Box();
myBox.setWidth(50);
myBox.setHeight(30);
try {
FileOutputStream fs = new FileOutputStream("F:\\foo.ser");
ObjectOutputStream os = new ObjectOutputStream(fs);
os.writeObject(myBox);
os.close();
FileInputStream fi = new FileInputStream("F:\\foo.ser");
ObjectInputStream oi = new ObjectInputStream(fi);
Box box = (Box)oi.readObject();
oi.close();
System.out.println(box.height+","+box.width);
} catch (Exception e) {
e.printStackTrace();
}
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
}
从上面序列化的工作流程可以看出,要想序列化对象,使用 ObjectOutputStream 对象输出流的 writeObject() 方法写入对象状态信息,即可使用 readObject() 方法读取信息。
那是不是可以在 ArrayList 中调用 ObjectOutputStream 对象的 writeObject() 方法将 elementData 的值写入输出流呢?
见源码:
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
虽然 elementData 被 transient 修饰,不能被序列化,但是我们可以将它的值取出来,然后将该值写入输出流。
ArrayList 的反序列化处理原理同上,见源码:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
从上面源码又引出另外一个问题,这些方法都定义为 private 的,那什么时候能调用呢?
如果一个类不仅实现了 Serializable 接口,而且定义了 readObject(ObjectInputStream in) 和 writeObject(ObjectOutputStream out) 方法,那么将按照如下的方式进行序列化和反序列化:
ObjectOutputStream 会调用这个类的 writeObject 方法进行序列化,ObjectInputStream 会调用相应的 readObject 方法进行反序列化。
事情到底是这样的吗?我们做个小实验,来验明正身。
实验1:
import java.io.*;
public class TestSerialization implements Serializable{
private static final long serialVersionUID = 5732067711721143635L;
private transient int num;
public int getNum(){
return num;
}
public void setNum(int num){
this.num = num;
}
private void writeObject(ObjectOutputStream s){
try {
s.defaultWriteObject();
s.writeObject(num);
System.out.println("writeObject of "+this.getClass().getName());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void readObject(ObjectInputStream s){
try {
s.defaultReadObject();
num = (Integer) s.readObject();
System.out.println("readObject of "+this.getClass().getName());
} catch (ClassNotFoundException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException{
TestSerialization test = new TestSerialization();
test.setNum(10);
System.out.println("序列化之前的值:"+test.getNum());
// 写入
ObjectOutputStream outputStream = new ObjectOutputStream(
new FileOutputStream("D:\\test.tmp"));
outputStream.writeObject(test);
outputStream.close();
// 读取
ObjectInputStream inputStream = new ObjectInputStream(
new FileInputStream("D:\\test.tmp"));
TestSerialization aTest = (TestSerialization) inputStream.readObject();
inputStream.close();
System.out.println("读取序列化后的值:"+aTest.getNum());
}
}
输出:
序列化之前的值:10
writeObject of TestSerialization
readObject of TestSerialization
读取序列化后的值:10
实验结果证明,事实确实是如此:
ObjectOutputStream 会调用这个类的 writeObject 方法进行序列化,ObjectInputStream 会调用相应的readObject 方法进行反序列化。
那么 ObjectOutputStream 又是如何知道一个类是否实现了 writeObject 方法呢?又是如何自动调用该类的 writeObject 方法呢?
答案是:通过反射机制实现的。
部分解答:
ObjectOutputStream 的 writeObject 又做了哪些事情。它会根据传进来的 ArrayList 对象得到 Class,然后再包装成 ObjectStreamClass,在 writeSerialData 方法里,会调用 ObjectStreamClass 的 invokeWriteObject 方法,最重要的代码如下:
writeObjectMethod.invoke(obj, new Object[]{ out });
实例变量 writeObjectMethod 的赋值方式如下:
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
private static Method getPrivateMethod(Class<?> cl, String name,
Class<?>[] argTypes,
Class<?> returnType)
{
try {
Method meth = cl.getDeclaredMethod(name, argTypes);
// 通过反射访问对象的 private 方法
meth.setAccessible(true);
int mods = meth.getModifiers();
return ((meth.getReturnType() == returnType) &&
((mods & Modifier.STATIC) == 0) &&
((mods & Modifier.PRIVATE) != 0)) ? meth : null;
} catch (NoSuchMethodException ex) {
return null;
}
}
在做实验时,我们发现一个问题,那就是为什么需要 s.defaultWriteObject(); 和 s.defaultReadObject(); 语句在 readObject(ObjectInputStream o) 和 writeObject(ObjectOutputStream o) 之前呢?
它们的作用如下:
既然要将 ArrayList 的字段序列化(即将 elementData 序列化),那为什么又要用 transient 修饰 elementData 呢?
回想 ArrayList 的自动扩容机制,elementData 数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于 ArrayList 所存元素的个数。
比如,现在实际有了 8 个元素,那么 elementData 数组的容量可能是 8x1.5=12,如果直接序列化 elementData 数组,那么就会浪费 4 个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。
所以 ArrayList 的设计者将 elementData 设计为 transient,然后在 writeObject 方法中手动将其序列化,只序列化实际存储的那些元素,而不是整个数组。
见源码:
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
从源码中,可以观察到循环时是使用 i
java.io.Serializable浅析
java serializable深入了解
ArrayList源码分析——如何实现Serializable
java序列化和反序列话总结