您的位置:首页 > 编程语言 > Java开发

java ArrayList的序列化分析

2016-01-30 16:15 381 查看

一、绪论

所谓的JAVA序列化与反序列化,序列化就是将JAVA对象以一种的形式保持,比如存放到硬盘,或是用于传输。反序列化是序列化的一个逆过程。

JAVA规定被序列化的对象必须实现java.io.Serializable这个接口,而我们分析的目标ArrayList同样实现了该接口。

通过对ArrayList源码的分析,可以知道ArrayList的数据存储都是依赖于elementData数组,它的声明为:

transientObject[]elementData;

注意transient修饰着elementData这个数组。

1、先看看transient关键字的作用

我们都知道一个对象只要实现了Serilizable接口,这个对象就可以被序列化,java的这种序列化模式为开发者提供了很多便利,我们可以不必关系具体序列化的过程,只要这个类实现了Serilizable接口,这个类的所有属性和方法都会自动序列化。

然而在实际开发过程中,我们常常会遇到这样的问题,这个类的有些属性需要序列化,而其他属性不需要被序列化,打个比方,如果一个用户有一些敏感信息(如密码,银行卡号等),为了安全起见,不希望在网络操作(主要涉及到序列化操作,本地序列化缓存也适用)中被传输,这些信息对应的变量就可以加上transient关键字。换句话说,这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。

总之,java的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

具体详见:Javatransient关键字使用小记

既然elementData被transient修饰,按理来说,它不能被序列化的,那么ArrayList又是如何解决序列化这个问题的呢?

二、序列化工作流程

类通过实现java.io.Serializable接口可以启用其序列化功能。要序列化一个对象,必须与一定的对象输出/输入流联系起来,通过对象输出流将对象状态保存下来,再通过对象输入流将对象状态恢复。

在序列化和反序列化过程中需要特殊处理的类必须使用下列准确签名来实现特殊方法:

privatevoidwriteObject(java.io.ObjectOutputStreamout)throwsIOException

privatevoidreadObject(java.io.ObjectInputStreamin)throwsIOException,ClassNotFoundException

1、对象序列化步骤

a)写入

首先创建一个OutputStream输出流;

然后创建一个ObjectOutputStream输出流,并传入OutputStream输出流对象;

最后调用ObjectOutputStream对象的writeObject()方法将对象状态信息写入OutputStream。

b)读取

首先创建一个InputStream输入流;

然后创建一个ObjectInputStream输入流,并传入InputStream输入流对象;

最后调用ObjectInputStream对象的readObject()方法从InputStream中读取对象状态信息。

举例说明:



[code]publicclassBoximplementsSerializable{
privatestaticfinallongserialVersionUID=-3450064362986273896L;

privateintwidth;
privateintheight;

publicstaticvoidmain(String[]args){
BoxmyBox=newBox();
myBox.setWidth(50);
myBox.setHeight(30);
try{
FileOutputStreamfs=newFileOutputStream("F:\\foo.ser");
ObjectOutputStreamos=newObjectOutputStream(fs);
os.writeObject(myBox);
os.close();
FileInputStreamfi=newFileInputStream("F:\\foo.ser");
ObjectInputStreamoi=newObjectInputStream(fi);
Boxbox=(Box)oi.readObject();
oi.close();
System.out.println(box.height+","+box.width);
}catch(Exceptione){
e.printStackTrace();
}
}

publicintgetWidth(){
returnwidth;
}
publicvoidsetWidth(intwidth){
this.width=width;
}
publicintgetHeight(){
returnheight;
}
publicvoidsetHeight(intheight){
this.height=height;
}
}

[/code]

三、ArrayList解决序列化

1、序列化

从上面序列化的工作流程可以看出,要想序列化对象,使用ObjectOutputStream对象输出流的writeObject()方法写入对象状态信息,即可使用readObject()方法读取信息。

那是不是可以在ArrayList中调用ObjectOutputStream对象的writeObject()方法将elementData的值写入输出流呢?

见源码:

privatevoidwriteObject(java.io.ObjectOutputStreams)throwsjava.io.IOException
{
//Writeoutelementcount,andanyhiddenstuff
intexpectedModCount=modCount;
s.defaultWriteObject();
//Writeoutsizeascapacityforbehaviouralcompatibilitywithclone()
s.writeInt(size);
//Writeoutallelementsintheproperorder.
for(inti=0;i<size;i++)
{
s.writeObject(elementData[i]);
}
if(modCount!=expectedModCount)
{
thrownewConcurrentModificationException();
}
}


虽然elementData被transient修饰,不能被序列化,但是我们可以将它的值取出来,然后将该值写入输出流。


[code]//片段1它的功能等价于片段2
s.writeObject(elementData[i]);//传值时,是将实参elementData[i]赋给s.writeObject()的形参
//片段2
Objecttemp=newObject();//temp并没有被transient修饰
temp=elementData[i];
s.writeObject(temp);

[/code]

2、反序列化

ArrayList的反序列化处理原理同上,见源码:

[code]privatevoidreadObject(java.io.ObjectInputStreams)throwsjava.io.IOException,ClassNotFoundException
{
elementData=EMPTY_ELEMENTDATA;
//Readinsize,andanyhiddenstuff
s.defaultReadObject();
//Readincapacity
s.readInt();//ignored
if(size>0)
{
//belikeclone(),allocatearraybaseduponsizenotcapacity
ensureCapacityInternal(size);
Object[]a=elementData;
//Readinallelementsintheproperorder.
for(inti=0;i<size;i++)
{
a[i]=s.readObject();
}
}
}

[/code]
从上面源码又引出另外一个问题,这些方法都定义为private的,那什么时候能调用呢?

3、调用

如果一个类不仅实现了Serializable接口,而且定义了readObject(ObjectInputStreamin)和writeObject(ObjectOutputStreamout)方法,那么将按照如下的方式进行序列化和反序列化:

ObjectOutputStream会调用这个类的writeObject方法进行序列化,ObjectInputStream会调用相应的readObject方法进行反序列化。


事情到底是这样的吗?我们做个小实验,来验明正身。
实验1:


[code]publicclassTestSerializationimplementsSerializable
{
privatetransientintnum;

publicintgetNum()
{
returnnum;
}

publicvoidsetNum(intnum)
{
this.num=num;
}

privatevoidwriteObject(java.io.ObjectOutputStreams)
throwsjava.io.IOException
{
s.defaultWriteObject();
s.writeObject(num);
System.out.println("writeObjectof"+this.getClass().getName());
}

privatevoidreadObject(java.io.ObjectInputStreams)
throwsjava.io.IOException,ClassNotFoundException
{
s.defaultReadObject();
num=(Integer)s.readObject();
System.out.println("readObjectof"+this.getClass().getName());
}

publicstaticvoidmain(String[]args)
{
TestSerializationtest=newTestSerialization();
test.setNum(10);
System.out.println("序列化之前的值:"+test.getNum());
//写入
try
{
ObjectOutputStreamoutputStream=newObjectOutputStream(
newFileOutputStream("D:\\test.tmp"));
outputStream.writeObject(test);
}catch(FileNotFoundExceptione)
{
e.printStackTrace();
}catch(IOExceptione)
{
e.printStackTrace();
}
//读取
try
{
ObjectInputStreamoInputStream=newObjectInputStream(
newFileInputStream("D:\\test.tmp"));
try
{
TestSerializationaTest=(TestSerialization)oInputStream.readObject();
System.out.println("读取序列化后的值:"+aTest.getNum());
}catch(ClassNotFoundExceptione)
{
e.printStackTrace();
}
}catch(FileNotFoundExceptione)
{
e.printStackTrace();
}catch(IOExceptione)
{
e.printStackTrace();
}
}
}

[/code]

输出:
序列化之前的值:10
writeObjectofTestSerialization
readObjectofTestSerialization
读取序列化后的值:10

实验结果证明,事实确实是如此:
ObjectOutputStream会调用这个类的writeObject方法进行序列化,ObjectInputStream会调用相应的readObject方法进行反序列化。
那么ObjectOutputStream又是如何知道一个类是否实现了writeObject方法呢?又是如何自动调用该类的writeObject方法呢?
答案是:是通过反射机制实现的。
部分解答:
ObjectOutputStream的writeObject又做了哪些事情。它会根据传进来的ArrayList对象得到Class,然后再包装成ObjectStreamClass,在writeSerialData方法里,会调用ObjectStreamClass的invokeWriteObject方法,最重要的代码如下:

writeObjectMethod.invoke(obj,newObject[]{out});


实例变量writeObjectMethod的赋值方式如下:

[code]writeObjectMethod=getPrivateMethod(cl,"writeObject",
newClass[]{ObjectOutputStream.class},
Void.TYPE);

privatestaticMethodgetPrivateMethod(Classcl,Stringname,
Class[]argTypes,ClassreturnType)
{
try
{
Methodmeth=cl.getDeclaredMethod(name,argTypes);
//*****通过反射访问对象的private方法
meth.setAccessible(true);
intmods=meth.getModifiers();
return((meth.getReturnType()==returnType)
&&((mods&Modifier.STATIC)==0)&&((mods&Modifier.PRIVATE)!=0))?meth
:null;
}catch(NoSuchMethodExceptionex)
{
returnnull;
}
}

[/code]
在做实验时,我们发现一个问题,那就是为什么需要s.defaultWriteObject();和s.defaultReadObject();语句在
readObject(ObjectInputStreamo)
and
writeObject(ObjectOutputStreamo)
之前呢?
它们的作用如下:
1、Itreadsandwritesallthe
nontransient
fieldsoftheclassrespectively.
2、Thesemethodsalsohelpsinbackwardandfuturecompatibility.Ifinfutureyouaddsome
non-transient
fieldtotheclassandyouaretryingtodeserializeitbytheolderversionofclassthenthedefaultReadObject()methodwillneglectthenewlyaddedfield,similarlyifyoudeserializetheoldserializedobjectbythenewversionthenthenewnontransientfieldwilltakedefaultvaluefromJVM

四、为什么使用transient修饰elementData?

既然要将ArrayList的字段序列化(即将elementData序列化),那为什么又要用transient修饰elementData呢?

回想ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。

比如,现在实际有了8个元素,那么elementData数组的容量可能是8x1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。

所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。

见源码:


[code]//Writeoutallelementsintheproperorder.
for(inti=0;i<size;i++)
{
s.writeObject(elementData[i]);
}

从源码中,可以观察到循环时是使用i<size而不是i<elementData.length,说明序列化时,只需实际存储的那些元素,而不是整个数组。[/code]
参考:

1、java.io.Serializable浅析

2、javaserializable深入了解

3、ArrayList源码分析——如何实现Serializable

4、java序列化和反序列话总结
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
章节导航