从基础的IO包开始阅读。
IO:表示层,将各种数据编码/解码,方便于在网络上传输。
下图是一个大致的结构图,可以看出,大部分的类都是以Writable结
尾的数据结构和基本数据类型。
一、基本数据类型
Hadoop 中,并没有使用java自带的基本类型类(Integer、Float等)而是使用了自己开发的类IntWritable、FloatWritable、BooleanWritable、LongWritable、ByteWritable、BytesWritable、DoubleWritable,他们都实现了接口WritableComparable,接口定义如下:
public interface WritableComparable<T> extends Writable, Comparable<T> {} |
Comparable是java.lang包的接口。
Writable接口定义如下
public interface Writable { void write(DataOutput out) throws IOException; void readFields(DataInput in) throws IOException; |
Writable接口是一个序列化对象的接口,能够将数据写入流或者从流中读出。实现了之后,能够进行特定类型数据的异地传输。
除了这些基本类型的定义,还添加了VLongWritable和VIntWritable,V指的是可变长度,例如long型的1实际只需要一个字节的空间,由于是long型的,所以会占用8字节的空间,而VLongWritable中,会根据数值的大小,分配适当的空间(仅分配一个字节),达到节省空间的作用。在基本数据类型的Writable类中,readFields(DataInput in)方法是直接调用in.readLong()(以LongWritable为例),而在VLongWritable与VIntegerWritable中,readFields(DataInputin)方法是使用了静态类WritableUtils中的readVLong(in)方法。WritableUtils是一个工具类,用于提供io中的Writable类的一些静态方法。
下面分析下VLongWritable中的write和readFields方法的实现。
Write方法:
将long型的数根据所占用的字节数写入DataOutput 中,例如-112~ 127,只需要一个字节存储(-128~-113用于做标识了)。其他的数,则需要先指明该数的正负与占用的长度(在第一个字节表示),然后再按照长度存储。
第一个字符中, -113(11110001)到-120(11111000)表示正数,-121(11111000)到-128(10000000)表示负数。
后续的N个字节,表示该数字的N字节。
代码如下:
public static void writeVLong(DataOutput stream, long i) throws IOException { if (i >= -112 && i <= 127) { stream.writeByte((byte)i); return; }
int len = -112; if (i < 0) { i ^= -1L; // take one's complement' 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)); } } |
readVLong方法,若第一个字节大于-112,则仅为一位数,直接返回该值;否则,则判断后面字节的位数,并读出返回。
由于对整数的操作都是以字节方式进行,故都是使用位操作进行。如
i= i << 8;
i= i | (b & 0xFF);
将i左移8位,并将其与新读入的8位数进行或运算,这样,就完成了一个字节数据的写入。
public static long readVLong(DataInput stream) throws IOException { byte firstByte = stream.readByte(); int len = decodeVIntSize(firstByte); if (len == 1) { return firstByte; } long i = 0; for (int idx = 0; idx < len-1; idx++) { byte b = stream.readByte(); i = i << 8; i = i | (b & 0xFF); } return (isNegativeVInt(firstByte) ? (i ^ -1L) : i); }
public static int decodeVIntSize(byte value) { if (value >= -112) { return 1; } else if (value < -120) { return -119 - value; } return -111 - value; } |
在上述每个类初始化的时候,都会将自定义的比较器(Comparator)注册进WritableComparator的HashMap中,以供调用。
static { // register this comparator WritableComparator.define(FloatWritable.class, new Comparator()); } |
下图是这些基本数据类型的类图
二、数据结构
几个实现了Writable接口的数据结构如下
主要有4种Writable型数据结构:分别是ArrayWritable,TwoDArrayWritable,MapWritable和SortedMapWritable
1、 ArrayWritable
看到ArrayWritable的构造函数有个形式如下:
private Class<? extends Writable> valueClass; private Writable[] values; public ArrayWritable(Class<? extends Writable> valueClass) { if (valueClass == null) { throw new IllegalArgumentException("null valueClass"); } this.valueClass = valueClass; }
public ArrayWritable(Class<? extends Writable> valueClass, Writable[] values) { this(valueClass); this.values = values; }
public ArrayWritable(String[] strings) { this( for (int i = 0; i < strings.length; i++) { values[i] = new UTF8(strings[i]); } |
第三个构造函数中,传入的是一个字符串数组,则自动将其进行打包,使用UTF8这个类进行封装,该类实现了WritableComparable 接口。这样,能够方便的处理字符串数组。因此,除了String类型的数据,该数据结构不能够存储其他未实现Writable接口的数据。
2、 TwoDArrayWritable 是二维数组。实现不复杂,主要还是实现了Writable接口,
public Object toArray() { int dimensions[] = {values.length, 0}; Object result = Array.newInstance(valueClass, dimensions); for (int i = 0; i < values.length; i++) { Object resultRow = Array.newInstance(valueClass, values[i].length); Array.set(result, i, resultRow); for (int j = 0; j < values[i].length; j++) { Array.set(resultRow, j, values[i][j]); } } return result; } |
该toArray方法将二维数组以对象的形式返回,使用了自带的Array里面的newInstance和set方法,而不是传统的
Array arr = newArrayList();
Arr.add(newArrayList())
的方式。自带的Array这种方式能够创建任意维度的数组(<255),并且似乎看上去更加优美,他不会绑定于具有的Array实现类上,更灵活。
3、MapWritable 和SortedMapWritable 都继承了抽象类AbstractMapWritable,这两个类像是设计模式中的适配器,大部分的函数都是直接调用类成员变量的相应方法,并实现了Writable接口(通过继承AbstractMapWritable间接实现了Writable接口),使其满足接口约束。
从以上可以看出,由于java的数据类型及数据结构不便于hadoop进行数据流的写入和读出。因此,hadoop将一些常用的数据类型及数据结构进行封装,在外面加了层壳(让他们都实现Writable接口),这样,能够以统一的方式进行数据的读写。方便后面阶段的数据写入和读出操作。
参考
1、Hadoop源代码分析【IO专题】
http://www.cnblogs.com/qlee/archive/2011/05/18/2049864.html
2、[Hadoop源码解读](五)MapReduce篇之Writable相关类