本文地址:http://www.cnblogs.com/archimedes/p/hadoop-writable.html,轉載請注明源地址。
Hadoop將很多Writable類歸入org.apache.hadoop.io包中,在這些類中,比較重要的有Java基本類、Text、Writable集合、ObjectWritable等,重點介紹Java基本類和ObjectWritable的實現。
1. Java基本類型的Writable封裝
目前Java基本類型對應的Writable封裝如下表所示。所有這些Writable類都繼承自WritableComparable。也就是說,它們是可比較的。同時,它們都有get()和set()方法,用於獲得和設置封裝的值。
Java基本類型對應的Writable封裝
Java基本類型 | Writable | 序列化后長度 |
布爾型(boolean) | BooleanWritable | 1 |
字節型(byte) | ByteWritable | 1 |
整型(int) | IntWritable VIntWritable |
4 1~5 |
浮點型(float) | FloatWritable | 4 |
長整型(long) | LongWritable VLongWritable |
8 1~9 |
雙精度浮點型(double) | DoubleWritable | 8 |
在表中,對整型(int和long)進行編碼的時候,有固定長度格式(IntWritable和LongWritable)和可變長度格式(VIntWritable和VLongWritable)兩種選擇。固定長度格式的整型,序列化后的數據是定長的,而可變長度格式則使用一種比較靈活的編碼方式,對於數值比較小的整型,它們往往比較節省空間。同時,由於VIntWritable和VLongWritable的編碼規則是一樣的,所以VIntWritable的輸出可以用VLongWritable讀入。下面以VIntWritable為例,說明Writable的Java基本類封裝實現。代碼如下:
public class VIntWritable implements WritableComparable { private int value; …… // 設置VIntWritable的值 public void set(int value) { this.value = value; } // 獲取VIntWritable的值 public int get() { return value; } public void readFields(DataInput in) throws IOException { value = WritableUtils.readVInt(in); } public void write(DataOutput out) throws IOException { 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)計算。
后續編碼將高位在前,寫入輸入的整數(除去前面全0字節)。代碼如下:
public final class WritableUtils { public stati cvoid writeVInt(DataOutput stream, int i) throws IOException { writeVLong(stream, i); } /** * @param stream保存系列化結果輸出流 * @param i 被序列化的整數 * @throws java.io.IOException */ public static void writeVLong(DataOutput stream, long i) throws…… { //處於[-112, 127]的整數 if (i >= -112 && i <= 127) { stream.writeByte((byte)i); return; } //計算情況2的第一個字節 int len = -112; if (i < 0) { i ^= -1L; len = -120; } long tmp = i; while (tmp != 0) { tmp = tmp >> 8; len--; } stream.writeByte((byte)len); len = (len < -120) ? -(len + 120) : -(len + 112); //輸出后續字節 for (int idx = len; idx != 0; idx--) { int shiftbits = (idx - 1) * 8; long mask = 0xFFL << shiftbits; stream.writeByte((byte)((i & mask) >> shiftbits)); } } }
2. ObjectWritable類的實現
針對Java基本類型、字符串、枚舉、Writable、空值、Writable的其他子類,ObjectWritable提供了一個封裝,適用於字段需要使用多種類型。ObjectWritable可應用於Hadoop遠程過程調用中參數的序列化和反序列化;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時,序列化的結果包含對象類名,對象實際類名和對象序列化結果三部分。
為什么需要對象實際類名呢?根據Java的單根繼承規則,ObjectWritable中傳入的declaredClass,可以是傳入instance對象對應的類的類對象,也可以是instance對象的父類的類對象。但是,在序列化和反序列化的時候,往往不能使用父類的序列化方法(如write方法)來序列化子類對象,所以,在序列化結果中必須記住對象實際類名。相關代碼如下:
public class ObjectWritable implements Writable, Configurable { private Class declaredClass;//保存於ObjectWritable的對象對應的類對象 private Object instance;//被保留的對象 private Configuration conf; public ObjectWritable() {} public ObjectWritable(Object instance) { set(instance); } public ObjectWritable(Class declaredClass, Object instance) { this.declaredClass = declaredClass; this.instance = instance; } …… public void readFields(DataInput in) throws IOException { readObject(in, this, this.conf); } public void write(DataOutput out) throws IOException { writeObject(out, instance, declaredClass, conf); } …… public static void writeObject(DataOutput out, Object instance, Class declaredClass,Configuration conf) throws……{ if (instance == null) {//空 instance = new NullInstance(declaredClass, conf); declaredClass = Writable.class; } // 寫出declaredClass的規范名 UTF8.writeString(out, declaredClass.getName()); if (declaredClass.isArray()) {//數組 …… } else if (declaredClass == String.class) {//字符串 …… } else if (declaredClass.isPrimitive()) {//基本類型 if (declaredClass == Boolean.TYPE) { //boolean out.writeBoolean(((Boolean)instance).booleanValue()); } else if (declaredClass == Character.TYPE) { //char …… } } else if (declaredClass.isEnum()) {//枚舉類型 …… } else if (Writable.class.isAssignableFrom(declaredClass)) { //Writable的子類 UTF8.writeString(out, instance.getClass().getName()); ((Writable)instance).write(out); } else { …… } public static Object readObject(DataInput in, ObjectWritable objectWritable, Configuration conf){ …… Class instanceClass = null; …… Writable writable = 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子對象。相關代碼如下:
public class WritableFactories { //保存了類型和WritableFactory工廠的對應關系 private static final HashMap<Class, WritableFactory>CLASS_TO_FACTORY = new HashMap<Class, WritableFactory>(); …… public static Writable newInstance(Class<? extends Writable> c, Configuration conf) { WritableFactory factory = WritableFactories.getFactory(c); if (factory != null) { Writable result = factory.newInstance(); if (result instanceof Configurable) { ((Configurable) result).setConf(conf); } return result; } else { //采用傳統的反射工具ReflectionUtils,創建對象 return ReflectionUtils.newInstance(c, conf); } } }
WritableFactories.newInstance()方法根據輸入的類型查找對應的WritableFactory工廠對象,然后調用該對象的newInstance()創建對象,如果該對象是可配置的,newInstance()還會通過對象的setConf()方法配置對象。
WritableFactories提供注冊機制,使得這些Writable子類可以將該工廠登記到WritableFactories的靜態成員變量CLASS_TO_FACTORY中。下面是一個典型的WritableFactory工廠實現,來自於HDFS的數據塊Block。其中,WritableFactories.setFactory()需要兩個參數,分別是注冊類對應的類對象和能夠構造注冊類的WritableFactory接口的實現,在下面的代碼里,WritableFactory的實現是一個匿名類,其newInstance()方法會創建一個新的Block對象。
public class Block implements Writable, Comparable<Block> { static { WritableFactories.setFactory (Block.class,//類對象 new WritableFactory() {//對應類的WritableFactory實現 public Writable newInstance() { return new Block(); } }); } …… }
ObjectWritable作為一種通用機制,相當浪費資源,它需要為每一個輸出寫入封裝類型的名字。如果類型的數量不是很多,而且可以事先知道,則可以使用一個靜態類型數組來提高效率,並使用數組索引作為類型的序列化引用。GenericWritable就是因為這個目的被引入org.apache.hadoop.io包中。