《Hadoop技术内幕:深入解析Hadoop Common和HDFS架构设计与实现原理》第3章序列化与压缩,本章涉及了org.apache.hadoop.io包下最重要的两部分内容:序列化和压缩。本节为大家介绍序列化。
第3章 序列化与压缩
传统的计算机系统通过I/O操作与外界进行交流,Hadoop的I/O由传统的I/O系统发展而来,但又有些不同,Hadoop需要处理P、T级别的数据,所以在org.apache.hadoop.io包中包含了一些面向海量数据处理的基本输入输出工具,本章会对其中的序列化和压缩进行研究。
3.1 序列化
对象的序列化(Serialization)用于将对象编码成一个字节流,以及从字节流中重新构建对象。“将一个对象编码成一个字节流”称为序列化该对象(Serializing);相反的处理过程称为反序列化(Deserializing)。
序列化有三种主要的用途:
作为一种持久化格式:一个对象被序列化以后,它的编码可以被存储到磁盘上,供以后反序列化用。
作为一种通信数据格式:序列化结果可以从一个正在运行的虚拟机,通过网络被传递到另一个虚拟机上。
作为一种拷贝、克隆(clone)机制:将对象序列化到内存的缓存区中,然后通过反序列化,可以得到一个对已存对象进行深拷贝的新对象。
在分布式数据处理中,主要使用上面提到的前两种功能:数据持久化和通信数据格式。
在分析Hadoop的序列化机制前,先介绍一下Java内建的序列化机制。
3.1.1 Java内建序列化机制
Java序列化机制将对象转换为连续的byte数据,这些数据可以在日后还原为原先的对象状态,该机制还能自动处理不同操作系统上的差异,在Windows系统上序列化的Java对象,可以在UNIX系统上被重建出来,不需要担心不同机器上的数据表示方法,也不需要担心字节排列次序,如大端(big endian)、小端(little endian)或其他细节。
在Java中,使一个类的实例可被序列化非常简单,只需要在类声明中加入implements Serializable即可。Serializable接口是一个标志,不具有任何成员函数,其定义如下:
- publicinterfaceSerializable{
- }
Serializable接口没有任何方法,所以不需要对类进行修改,Block类通过声明它实现了Serializable接口,立即可以获得Java提供的序列化功能。代码如下:
- publicclassBlockimplementsWritable,Comparable<Block>,Serializable
由于序列化主要应用在与I/O相关的一些操作上,其实现是通过一对输入/输出流来实现的。如果想对某个对象执行序列化动作,可以在某种OutputStream对象(后面还会讨论Java的流)的基础上创建一个对象流ObjectOutputStream对象,然后调用writeObject()就可达到目的。
writeObject()方法负责写入实现了Serializable接口对象的状态信息,输出数据将被送至该OutputStream。多个对象的序列化可以在ObjectOutputStream对象上多次调用writeObject(),分别写入这些对象。下面是序列化一个Block对象的例子:
- Blockblock1=newBlock(7806259420524417791L,39447755L,56736651L);
- Blockblock2=newBlock(5547099594945187683L,67108864L,56736828L);
- ……
- ByteArrayOutputStreamout=newByteArrayOutputStream();
- //在ByteArrayOutputStream的基础上创建ObjectOutputStream
- ObjectOutputStreamobjOut=newObjectOutputStream(out);
- //对block进行序列化
- objOut.writeObject(block1);
对于Java基本类型的序列化,ObjectOutputStream提供了writeBoolean()、writeByte()等方法。
输入过程类似,将InputStream包装在ObjectInputStream中并调用readObject(),该方法返回一个指向向上转型后的Object的引用,通过向下转型,就可以得到正确结果。读取对象时,必须要小心地跟踪存储的对象的数量、顺序以及它们的类型。
Java的序列化机制非常“聪明”,JavaDoc中对ObjectOutputStream的writeObject()方法的说明是:“……这个对象的类、类签名、类的所有非暂态和非静态成员的值,以及它所有的父类都要被写入”,序列化机制会自动访问对象的父类,以保证对象内容的一致性。同时,序列化机制不仅存储对象在内存中的原始数据,还会追踪通过该对象可以到达的其他对象的内部数据,并描述所有这些对象是如何被链接起来的。对于复杂的情形,Java序列化机制也能应付自如:在输出objectA和objectB时,不会重复保存对象的序列化结果(如objectC,即objectC只被序列化一次);对于循环引用的对象,序列化也不会陷入死循环(如图3-1右图的情形)。
但是,序列化以后的对象在尺寸上有点过于充实了,以Block类为例,它只包含3个长整数,但是它的序列化结果竟然有112字节,而BlockMetaDataInfo其实只多了一个long型的成员变量,输出结果已经膨胀到190字节。包含3个长整数的Block对象的序列化结果如下:
- ACED00057372001C6F72672E6861646F....sr..org.hado
- 6F70696E7465726E616C2E7365722E42opinternal.ser.B
- 6C6F636BE780E3D3A6B622530200034Alock......"S...J
- 0007626C6F636B49644A000F67656E65..blockIdJ..gene
- 726174696F6E5374616D704A00086E75rationStampJ..nu
- 6D427974657378706C55679568E792FFmBytesxplUg.h...
- 000000000361BB8B000000000259ECCB.....a.......Y..
仔细看Block的输出会发现,序列化的结果中包含了大量与类相关的信息。Java的序列过程在《Java Object Serialization Specification》中规范,以Block为例,其结果的前两个字节是魔数(Magic Number)“AC ED”;后续两个字节是序列化格式的版本号,现在使用的版本号是5;接下来是类的描述信息,包括类的版本ID、是否实现writeObject()和readObject()方法等信息,对于拥有超类的类(如BlockMetaDataInfo),超类的信息也会递归地被保存下来;这些信息都写入OutputStream对象后,接下来才是对象的数据。在这个过程中,序列化输出中保存了大量的附加信息,导致序列化结果膨胀,对于需要保存和处理大规模数据的Hadoop来说,需要一个新的序列化机制。
由于篇幅的关系,不再详细讨论Java的序列化机制,有兴趣的读者可以参考《Java Object Serialization Specification》。
3.1.2 Hadoop序列化机制
和Java序列化机制不同(在对象流ObjectOutputStream对象上调用writeObject()方法),Hadoop的序列化机制通过调用对象的write()方法(它带有一个类型为DataOutput的参数),将对象序列化到流中。反序列化的过程也是类似,通过对象的readFields(),从流中读取数据。值得一提的是,Java序列化机制中,反序列化过程会不断地创建新的对象,但在Hadoop的序列化机制的反序列化过程中,用户可以复用对象:如,在Block的某个对象上反复调用readFields(),可以在同一个对象上得到多个反序列化的结果,而不是多个反序列化的结果对象(对象被复用了),这减少了Java对象的分配和回收,提高了应用的效率。
- publicstaticvoidmain(String[]args){
- try{
- Blockblock1=newBlock(7806259420524417791L,39447755L,56736651L);
- ……
- ByteArrayOutputStreambout=newByteArrayOutputStream();
- DataOutputStreamdout=newDataOutputStream(bout);
- block1.write(dout);//序列化对象到输出流dout中
- dout.close();
- System.out.println(……);
- SerializationExample.print16(out.toByteArray(),bout.size());
- }
- ……
- }
由于Block对象序列化时只输出了3个长整数, block1的序列化结果一共有24字节,如下所示。和Java的序列化机制的输出结果对比,Hadoop的序列化结果紧凑而且快速。
- ACED0005737200286F72672E6861646F....sr.(org.hado
- 6F70696E7465726Eopintern
3.1.3 Hadoop序列化机制的特征
对于处理大规模数据的Hadoop平台,其序列化机制需要具有如下特征:
紧凑:由于带宽是Hadoop集群中最稀缺的资源,一个紧凑的序列化机制可以充分利用数据中心的带宽。
快速:在进程间通信(包括MapReduce过程中涉及的数据交互)时会大量使用序列化机制,因此,必须尽量减少序列化和反序列化的开销。
可扩展:随着系统的发展,系统间通信的协议会升级,类的定义会发生变化,序列化机制需要支持这些升级和变化。
互操作:可以支持不同开发语言间的通信,如C++和Java间的通信。这样的通信,可以通过文件(需要精心设计文件的格式)或者后面介绍的IPC机制实现。
Java的序列化机制虽然强大,却不符合上面这些要求。Java Serialization将每个对象的类名写入输出流中,这导致了Java序列化对象需要占用比原对象更多的存储空间。同时,为了减少数据量,同一个类的对象的序列化结果只输出一份元数据,并通过某种形式的引用,来共享元数据。引用导致对序列化后的流进行处理的时候,需要保持一些状态。想象如下一种场景,在一个上百G的文件中,反序列化某个对象,需要访问文件中前面的某一个元数据,这将导致这个文件不能切割,并通过MapReduce来处理。同时,Java序列化会不断地创建新的对象,对于MapReduce应用来说,不能重用对象,在已有对象上进行反序列化操作,而是不断创建反序列化的各种类型记录,这会带来大量的系统开销。
3.1.4 Hadoop Writable机制
为了支持以上这些特性,Hadoop引入org.apache.hadoop.io.Writable接口,作为所有可序列化对象必须实现的接口,其类图如图3-2所示。
Writable机制紧凑、快速(但不容易扩展到Java以外的语言,如C、Python等)。和java.io.Serializable不同,Writable接口不是一个说明性接口,它包含两个方法:
- publicinterfaceWritable{
- /**
- *输出(序列化)对象到流中
- *@paramoutDataOuput流,序列化的结果保存在流中
- *@throwsIOException
- */
- voidwrite(DataOutputout)throwsIOException;
- /**
- *从流中读取(反序列化)对象
- *为了效率,请尽可能复用现有的对象
- *@paraminDataInput流,从该流中读取数据
- *@throwsIOException
- */
- voidreadFields(DataInputin)throwsIOException;
- }
Writable.write()方法用于将对象状态写入二进制的DataOutput中,反序列化的过程由readFields()从DataInput流中读取状态完成。下面是一个例子:
- publicclassBlockimplementsWritable,Comparable<Block>,Serializable{
- ……
- privatelongblockId;
- privatelongnumBytes;
- privatelonggenerationStamp;
- ……
- publicvoidwrite(DataOutputout)throwsIOException{
- out.writeLong(blockId);
- out.writeLong(numBytes);
- out.writeLong(generationStamp);
- }
- publicvoidreadFields(DataInputin)throwsIOException{
- this.blockId=in.readLong();
- this.numBytes=in.readLong();
- this.generationStamp=in.readLong();
- if(numBytes<0){
- thrownewIOException("Unexpectedblocksize:"+numBytes);
- }
- }
- ……
- }
这个例子使用的是前面分析Java序列化机制的Block类,Block实现了Writable接口,即需要实现write()方法和readFields()方法,这两个方法的实现都很简单:Block有三个成员变量,write()方法简单地把这三个变量写入流中,而readFields()则从流中依次读入这些数据,并做必要的检查。
Hadoop序列化机制中还包括另外几个重要接口:WritableComparable、RawComparator和WritableComparator。
WritableComparable,顾名思义,它提供类型比较的能力,这对MapReduce至关重要。该接口继承自Writable接口和Comparable接口,其中Comparable用于进行类型比较。ByteWritable、IntWritable、DoubleWritable等Java基本类型对应的Writable类型,都继承自WritableComparable。
效率在Hadoop中非常重要,因此HadoopI/O包中提供了具有高效比较能力的RawComparator接口。RawComparator和WritableComparable类图如图3-3所示。
RawComparator接口允许执行者比较流中读取的未被反序列化为对象的记录,从而省去了创建对象的所有开销。其中,compare()比较时需要的两个参数所对应的记录位于字节数组b1和b2的指定开始位置s1和s1,记录长度为l1和l2,代码如下:
- publicinterfaceRawComparator<T>extends
- Comparator<T>{
- publicintcompare(byte[]b1,ints1,intl1,byte[]b2,ints2,intl2);
- }
以IntWritable为例,它的RawComparator实现中(WritableComparator是一个辅助类,实现了RawComparator接口),compare()方法通过readInt()直接在字节数组中读入需要比较的两个整数,然后输出Comparable接口要求的比较结果。值得注意的是,该过程中compare()方法避免使用IntWritable对象,从而避免了不必要的对象分配。相关代码如下:
- publicstaticclassComparatorextendsWritableComparator{
- ……
- publicintcompare(byte[]b1,ints1,intl1,
- byte[]b2,ints2,intl2){
- intthisValue=readInt(b1,s1);
- intthatValue=readInt(b2,s2);
- return(thisValue<thatValue?-1:(thisValue==thatValue?0:1));
- }
- ……
- }
WritableComparator是RawComparator对WritableComparable类的一个通用实现。它提供两个主要功能。首先,提供了一个RawComparator的compare()默认实现,该实现从数据流中反序列化要进行比较的对象,然后调用对象的compare()方法进行比较(这些对象都是Comparable的)。其次,它充当了RawComparator实例的一个工厂方法,例如,可以通过如下代码获得IntWritable的RawComparator:
- RawComparator<IntWritable>comparator=
- WritableComparator.get(IntWritable.class);
3.1.5 典型的Writable类详解(1)
Hadoop将很多Writable类归入org.apache.hadoop.io包中,类图如图3-4所示。
在这些类中,比较重要的有Java基本类、Text、Writable集合、ObjectWritable等,本节重点介绍Java基本类和ObjectWritable的实现。
1. Java基本类型的Writable封装
目前Java基本类型对应的Writable封装如表3-1所示。所有这些Writable类都继承自WritableComparable。也就是说,它们是可比较的。同时,它们都有get()和set()方法,用于获得和设置封装的值。
表3-1 Java基本类型对应的Writable封装
在表3-1中,对整型(int和long)进行编码的时候,有固定长度格式(IntWritable和LongWritable)和可变长度格式(VIntWritable和VLongWritable)两种选择。固定长度格式的整型,序列化后的数据是定长的,而可变长度格式则使用一种比较灵活的编码方式,对于数值比较小的整型,它们往往比较节省空间。同时,由于VIntWritable和VLongWritable的编码规则是一样的,所以VIntWritable的输出可以用VLongWritable读入。下面以VIntWritable为例,说明Writable的Java基本类封装实现。代码如下:
- publicclassVIntWritableimplementsWritableComparable{
- privateintvalue;
- ……
- //设置VIntWritable的值
- publicvoidset(intvalue){this.value=value;}
- //获取VIntWritable的值
- publicintget(){returnvalue;}
- publicvoidreadFields(DataInputin)throwsIOException{
- value=WritableUtils.readVInt(in);
- }
- publicvoidwrite(DataOutputout)throwsIOException{
- WritableUtils.writeVInt(out,value);
- }
- ……
- }
首先,每个Java基本类型的Writable封装,其类的内部都包含一个对应基本类型的成员变量value,get()和set()方法就是用来对该变量进行取值/赋值操作的。而Writable接口要求的readFields()和write()方法,VIntWritable则是通过调用Writable工具类中提供的readVInt()和writeVInt()读/写数据。方法readVInt()和writeVInt()的实现也只是简单调用了readVLong()和writeVLong(),所以,通过writeVInt()写的数据自然可以通过readVLong()读入。
writeVLong ()方法实现了对整型数值的变长编码,它的编码规则如下:
如果输入的整数大于或等于–112同时小于或等于127,那么编码需要1字节;否则,序列化结果的第一个字节,保存了输入整数的符号和后续编码的字节数。符号和后续字节数依据下面的编码规则(又一个规则):
如果是正数,则编码值范围落在–113和–120间(闭区间),后续字节数可以通过–(v+112)计算。
如果是负数,则编码值范围落在–121和–128间(闭区间),后续字节数可以通过–(v+120)计算。
3.1.5 典型的Writable类详解(2)
后续编码将高位在前,写入输入的整数(除去前面全0字节)。代码如下:
- publicfinalclassWritableUtils{
- publicstaticvoidwriteVInt(DataOutputstream,inti)throwsIOException
- {
- writeVLong(stream,i);
- }
- /**
- *@paramstream保存系列化结果输出流
- *@parami被序列化的整数
- *@throwsjava.io.IOException
- */
- publicstaticvoidwriteVLong(DataOutputstream,longi)throws……
- {
- //处于[-112,127]的整数
- if(i>=-112&&i<=127){
- stream.writeByte((byte)i);
- return;
- }
- //计算情况2的第一个字节
- intlen=-112;
- if(i<0){
- i^=-1L;
- len=-120;
- }
- longtmp=i;
- while(tmp!=0){
- tmptmp=tmp>>8;
- len--;
- }
- stream.writeByte((byte)len);
- len=(len<-120)?-(len+120):-(len+112);
- //输出后续字节
- for(intidx=len;idx!=0;idx--){
- intshiftbits=(idx-1)*8;
- longmask=0xFFL<<shiftbits;
- stream.writeByte((byte)((i&mask)>>shiftbits));
- }
- }
- }
注意 本书附带代码对writeVLong()的输出结果做了一些分析,有兴趣的读者可以运行org.hadoopinternal.ser.VLongShow,分析一些典型整数的writeVLong()输出结果。
2. ObjectWritable类的实现
针对Java基本类型、字符串、枚举、Writable、空值、Writable的其他子类,ObjectWritable提供了一个封装,适用于字段需要使用多种类型。ObjectWritable可应用于Hadoop远程过程调用(将在第4章介绍)中参数的序列化和反序列化;ObjectWritable的另一个典型应用是在需要序列化不同类型的对象到某一个字段,如在一个SequenceFile的值中保存不同类型的对象(如LongWritable值或Text值)时,可以将该值声明为ObjectWritable。
ObjectWritable的实现比较冗长,需要根据可能被封装在ObjectWritable中的各种对象进行不同的处理。ObjectWritable有三个成员变量,包括被封装的对象实例instance、该对象运行时类的Class对象和Configuration对象。
ObjectWritable的write方法调用的是静态方法ObjectWritable.writeObject(),该方法可以往DataOutput接口中写入各种Java对象。
writeObject()方法先输出对象的类名(通过对象对应的Class 对象的getName()方法获得),然后根据传入对象的类型,分情况系列化对象到输出流中,也就是说,对象通过该方法输出对象的类名,对象序列化结果对到输出流中。在ObjectWritable.writeObject()的逻辑中,需要分别处理null、Java数组、字符串String、Java基本类型、枚举和Writable的子类6种情况,由于类的继承,处理Writable时,序列化的结果包含对象类名,对象实际类名和对象序列化结果三部分。
3.1.5 典型的Writable类详解(3)
为什么需要对象实际类名呢?根据Java的单根继承规则,ObjectWritable中传入的declaredClass,可以是传入instance对象对应的类的类对象,也可以是instance对象的父类的类对象。但是,在序列化和反序列化的时候,往往不能使用父类的序列化方法(如write方法)来序列化子类对象,所以,在序列化结果中必须记住对象实际类名。相关代码如下:
- publicclassObjectWritableimplementsWritable,Configurable{
- privateClassdeclaredClass;//保存于ObjectWritable的对象对应的类对象
- privateObjectinstance;//被保留的对象
- privateConfigurationconf;
- publicObjectWritable(){}
- publicObjectWritable(Objectinstance){
- set(instance);
- }
- publicObjectWritable(ClassdeclaredClass,Objectinstance){
- this.declaredClass=declaredClass;
- this.instance=instance;
- }
- ……
- publicvoidreadFields(DataInputin)throwsIOException{
- readObject(in,this,this.conf);
- }
- publicvoidwrite(DataOutputout)throwsIOException{
- writeObject(out,instance,declaredClass,conf);
- }
- ……
- publicstaticvoidwriteObject(DataOutputout,Objectinstance,
- ClassdeclaredClass,Configurationconf)throws……{
- if(instance==null){//空
- instance=newNullInstance(declaredClass,conf);
- declaredClass=Writable.class;
- }
- //写出declaredClass的规范名
- UTF8.writeString(out,declaredClass.getName());
- if(declaredClass.isArray()){//数组
- ……
- }elseif(declaredClass==String.class){//字符串
- ……
- }elseif(declaredClass.isPrimitive()){//基本类型
- if(declaredClass==Boolean.TYPE){//boolean
- out.writeBoolean(((Boolean)instance).booleanValue());
- }elseif(declaredClass==Character.TYPE){//char
- ……
- }
- }elseif(declaredClass.isEnum()){//枚举类型
- ……
- }elseif(Writable.class.isAssignableFrom(declaredClass)){
- //Writable的子类
- UTF8.writeString(out,instance.getClass().getName());
- ((Writable)instance).write(out);
- }else{
- ……
- }
- publicstaticObjectreadObject(DataInputin,
- ObjectWritableobjectWritable,Configurationconf){
- ……
- ClassinstanceClass=null;
- ……
- Writablewritable=WritableFactories.newInstance(instanceClass,
- conf);
- writable.readFields(in);
- instance=writable;
- ……
- }
- }
和输出对应,ObjectWritable的readFields()方法调用的是静态方法ObjectWritable.readObject (),该方法的实现和writeObject()类似,唯一值得研究的是Writable对象处理部分,readObject ()方法依赖于WritableFactories类。WritableFactories类允许非公有的Writable子类定义一个对象工厂,由该工厂创建Writable对象,如在上面的readObject ()代码中,通过WritableFactories的静态方法newInstance(),可以创建类型为instanceClass的Writable子对象。相关代码如下:
- publicclassWritableFactories{
- //保存了类型和WritableFactory工厂的对应关系
- privatestaticfinalHashMap<Class,WritableFactory>CLASS_TO_FACTORY
- =newHashMap<Class,WritableFactory>();
- ……
- publicstaticWritablenewInstance(Class<?extendsWritable>c,
- Configurationconf){
- WritableFactoryfactory=WritableFactories.getFactory(c);
- if(factory!=null){
- Writableresult=factory.newInstance();
- if(resultinstanceofConfigurable){
- ((Configurable)result).setConf(conf);
- }
- returnresult;
- }else{
- //采用传统的反射工具ReflectionUtils,创建对象
- returnReflectionUtils.newInstance(c,conf);
- }
- }
- }
3.1.5 典型的Writable类详解(4)
WritableFactories.newInstance()方法根据输入的类型查找对应的WritableFactory工厂对象,然后调用该对象的newInstance()创建对象,如果该对象是可配置的,newInstance()还会通过对象的setConf()方法配置对象。
WritableFactories提供注册机制,使得这些Writable子类可以将该工厂登记到WritableFactories的静态成员变量CLASS_TO_FACTORY中。下面是一个典型的WritableFactory工厂实现,来自于HDFS的数据块Block。其中,WritableFactories.setFactory()需要两个参数,分别是注册类对应的类对象和能够构造注册类的WritableFactory接口的实现,在下面的代码里,WritableFactory的实现是一个匿名类,其newInstance()方法会创建一个新的Block对象。
- publicclassBlockimplementsWritable,Comparable<Block>{
- static{
- WritableFactories.setFactory
- (Block.class,//类对象
- newWritableFactory(){//对应类的WritableFactory实现
- publicWritablenewInstance(){returnnewBlock();}
- });
- }
- ……
- }
ObjectWritable作为一种通用机制,相当浪费资源,它需要为每一个输出写入封装类型的名字。如果类型的数量不是很多,而且可以事先知道,则可以使用一个静态类型数组来提高效率,并使用数组索引作为类型的序列化引用。GenericWritable就是因为这个目的被引入org.apache.hadoop.io包中,由于篇幅关系,不再详细介绍,有兴趣的读者可以继续分析GenericWritable的源代码。
3.1.6 Hadoop序列化框架
大部分的MapReduce程序都使用Writable键–值对作为输入和输出,但这并不是Hadoop的API指定的,其他序列化机制也能和Hadoop配合,并应用于MapReduce中。
目前,除了前面介绍过的Java序列化机制和Hadoop使用的Writable机制,还流行其他序列化框架,如Hadoop Avro、Apache Thrift和Google Protocol Buffer。
Avro是一个数据序列化系统,用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷、快速地处理大量数据;动态语言友好,Avro提供的机制使动态语言可以方便地处理 Avro数据。
Thrift是一个可伸缩的、跨语言的服务开发框架,由Facebook贡献给开源社区,是Facebook的核心框架之一。基于Thrift的跨平台能力封装的Hadoop文件系统Thrift API(参考contrib的thriftfs模块),提供了不同开发语言开发的系统访问HDFS的能力。
Google Protocol Buffer是Google内部的混合语言数据标准,提供了一种轻便高效的结构化数据存储格式。目前,Protocol Buffers提供了C++、Java、Python三种语言的API,广泛应用于Google内部的通信协议、数据存储等领域中。
Hadoop提供了一个简单的序列化框架API,用于集成各种序列化实现,该框架由Serialization实现(在org.apache.hadoop.io.serializer包中)。
Serialization是一个接口,使用抽象工厂的设计模式,提供了一系列和序列化相关并相互依赖对象的接口。通过Serialization应用可以获得类型的Serializer实例,即将一个对象转换为一个字节流的实现实例;Deserializer实例和Serializer实例相反,它用于将字节流转为一个对象。很明显,Serializer和Deserializer相互依赖,所以必须通过抽象工厂Serialization,才能获得对应的实现。相关代码如下:
- publicinterfaceSerialization<T>{
- //客户端用于判断序列化实现是否支持该类对象
- booleanaccept(Class<?>c);
- //获得用于序列化对象的Serializer实现
- Serializer<T>getSerializer(Class<T>c);
- //获得用于反序列化对象的Deserializer实现
- Deserializer<T>getDeserializer(Class<T>c);
- }
如果需要使用Serializer来执行序列化,一般需要通过open()方法打开Serializer,open()方法传入一个底层的流对象,然后就可以使用serialize()方法序列化对象到底层的流中。最后序列化结束时,通过close()方法关闭Serializer。Serializer接口的相关代码如下:
- publicinterfaceSerializer<T>{
- //为输出(序列化)对象做准备
- voidopen(OutputStreamout)throwsIOException;
- //将对象序列化到底层的流中
- voidserialize(Tt)throwsIOException;
- //序列化结束,清理
- voidclose()throwsIOException;
- }
Hadoop目前支持两个Serialization实现,分别是支持Writable机制的WritableSerialization和支持Java序列化的JavaSerialization。通过JavaSerialization可以在MapReduce程序中方便地使用标准的Java类型,如int或String,但如同前面所分析的,Java的Object Serialization不如Hadoop的序列化机制有效,非特殊情况不要轻易尝试。