Hadoop自带的org.apache.hadoop.io包中有广泛的Writable类可供选择。
Writable类对所有Java基本类型提供封装,char类型除外(可以存储在IntWritable中)。所有的封装包含get()和set()两个方法用于读取或才能出封装的值。
Java基本类型 Writable实现 序列化大小(字节) boolean BooleanWritable 1 byte ByteWritable 1 Short ShortWritable 2 int IntWritable
VintWritable4
1~5float FloatWritable 4 long LongWritable
VlongWritable8
1~9double DoubleWritable 8
对整数进行编码时,有两种选择,即定长格式(IntWritable和LongWritable)和变长格式(VIntWritable和VLongWritable)。需要编码的数值如果相当小(在-127和127之间,包括-127和127),变长格式就是只用一个字节进行编码;否则,使用第一个字节来表示数值的正负和后跟多少个字节。例如,值163需要两个字节:
byte[] data = serialize(new VIntWritable(163));
assertThat(StringUtils.byteToHexString(data), is("8fa3"));
如何在定长格式和变长格式之间进行选择呢?定长格式编码很适合数值在整个值域空间中分布非常均匀的情况,例如使用精心设计的哈希函数。然而,大多数数值变量的分布都不均匀的,一般而言变长格式会更节省空间。变长编码的另一个优点是可以在VIntWritable和VLongWritable转换,因为他们的编码实际上是一致的。所以选择变长格式之后,便有增长的空间,不必一开始就用8字节的long表示。
Text是针对UTF-8序列的Writable类。一般可以认为他是java.lang.String的Writable等价。
Text类使用整型(通过变长编码的方式)来存储字符串编码中所需的字节数,因此该最大值为2GB。另外,Text使用标准UTF-8编码,这使得能够更简便的与其他理解UTF-8编码的工具进行交互操作。
由于着重使用标准的UTF-8编码,因此Text类和Java String类之间存在一定的差异。对Text类的索引是根据编码后字节序列中的位置实现的,并非字符串中的Unicode字符,也不是Java char的编码单元(如String)。对于ASCII字符串,这三个索引位置的概念是一致的。charAt()方法的用法如下例所示:
Text t = new Text("hadoop");
assertThat(t.getLength(), is(6));
assertThat(t.getBytes().length, is(6));
assertThat(t.charAt(2), is((int) 'd'));
assertThat("Out of bounds", t.charAt(100), is(-1));
注意,charAt()方法返回的是一个表示Unicode编码位置的int类型值,而String返回一个char类型值。Text还有一个find()方法,该方法类似于String的indexOf()方法:
Text t = new Text("hadoop");
assertThat("Find a substring", t.find("do"), is(2));
assertThat("Finds first 'o' ", t.find("o"), is(3));
assertThat("Finds 'o' from position 4 or later", t.find("o", 4), is(4));
assertThat("No match", t.find("pig"), is(-1));
一旦开始使用需要多个字节来编码的字符时,Text和String之间的区别就昭然若揭了。考虑下表显示的Unicode字符。
Unicode U+0041 U+00DF U+6771 U+10400 名称 拉丁大写字母A 拉丁小写字母SHARPS 无(统一表示的汉字) DESERET CAPITAL LETTER LONG I UTF-8 编码单元
Java表示41
\u0041c39f
\u00DFe69db1
\u6771F0909080
\uuD801\uDC00
所有字符(除了表中最后一个字符U+10400),都可以使用单个Java Char类型来表示。U+10400是一个候补字符,并且需要两个Java char来表示,称为“字符代理对”(surrogate pair)。下面范例中的测试显示了处理一个字符串时String和Text之间的差别。
// 验证String类和Text类的差异性的测试
public class StringTextComparisonTest {
@Test
public void string() throws UnsupportedEncodingException {
String s = "\u0041\u00DF\u6771\uD801\uDC00";
assertThat(s.length(), is(5));
assertThat(s.getBytes("UTF-8").length, is(10));
assertThat(s.indexOf("\u0041"), is(0));
assertThat(s.indexOf("\u00DF"), is(1));
assertThat(s.indexOf("\u6771"), is(2));
assertThat(s.indexOf("\uD801\uDC00"), is(3));
assertThat(s.charAt(0), is('\u0041'));
assertThat(s.charAt(1), is('\u00DF'));
assertThat(s.charAt(2), is('\u6771'));
assertThat(s.charAt(3), is('\uD801'));
assertThat(s.charAt(4), is('\uDC00'));
assertThat(s.codePointAt(0), is(0x0041));
assertThat(s.codePointAt(1), is(0x00DF));
assertThat(s.codePointAt(2), is(0x6771));
assertThat(s.codePointAt(3), is(0x10400));
}
@Test
public void test() {
Text t = new Text("\u0041\u00DF\u6771\uD801\uDC00");
assertThat(t.getLength(), is(10));
assertThat(t.find("\u0041"), is(0));
assertThat(t.find("\u00DF"), is(1));
assertThat(t.find("\u6771"), is(3));
assertThat(t.find("\uD801\uDC00"), is(6));
assertThat(t.charAt(0), is(0x0041));
assertThat(t.charAt(1), is(0x00DF));
assertThat(t.charAt(3), is(0x6771));
assertThat(t.charAt(6), is(0x10400));
}
}
这个测试证实String的长度是其所含char编码单元的个数(5,由该字符串的前三个字符和最后的一个代理对组成),但Text对象的长度却是其UTF-8编码的字节数(10=1+2+3+4)。相似的,String类的indexOf()方法返回char编码单元中的索引位置,Text类的find()方法则返回字节偏移量。
当代理对不能代表整个Unicode字符时,String类中的charAt()方法会根据指定的索引位置返回char编码单元。根据char编码单元索引位置,需要codePointAt()方法来获取表示成int类型的单个Unicode字符。事实上,Text类中的charAt()方法与String中codePointAt()更加相似(相较名称而言)。唯一的区别是通过字节的偏移量进行索引。
利用字节偏移量实现的位置索引,对Text类中的Unicode字符进行迭代是非常复杂的,因为无法通过简单地增加索引值来实现该迭代。同时迭代的语法有写模糊(参见下面范例代码):将Text对象转换为java.nio.ByteBuffer对象,然后利用缓冲区对Text对象反复调用bytesToCodePoint()静态方法。该方法能够获取下一代码的位置,并返回相应的int值,最后更新缓冲区中的位置。当bytesToCodePoint()返回-1时,则检测到字符串的末尾。
// 遍历Text对象中的字符
public class TextIterator {
public static void main(String[] args) {
Text t = new Text("\u0041\u00DF\u6771\uD801\uDC00");
ByteBuffer buf = ByteBuffer.wrap(t.getBytes(), 0, t.getLength());
int cp;
while(buf.hasRemaining() && (cp = Text.bytesToCodePoint(buf)) != -1) {
System.out.println(Integer.toHexString(cp));
}
}
}
运行这个程序,打印出字符串中四个字符的编码点(code point):
% hadoop TextIterator
41
df
6771
10400
与String相比,Text的另一个区别在于他是可变的(与所有Hadoop的Writable接口实现相似,NullWritable除外,他是单实例对象)。可以通过调用其中一个set()方法来重用Text实例,例如:
Text t = new Text("hadoop");
t.set("pig");
assertThat(t.getLength(), is(3));
assertThat(t.getBytes().length, is(3));
注意:在某些情况下,getBytes()方法返回的字节数组可能比getLength()函数返回的长度更长:
Text t = new Text("hadoop");
t.set(new Text("pig"));
assertThat(t.getLength(), is(3));
assertThat("Byte length not shortened", t.getBytes90.length, is(6));
以上代码说明了在调用getBytes()之前为什么始终都要调用getLength()方法,因为可以由此知道字节数组中多少字符是有效地。
Text类并不像java.lang.String类那样有丰富的字符串操作API。所以,在多数情况下需要将Text对象转换成String对象。这一转换通常通过调用toString()方法实现:
assertThat(new Text("hadoop").toString(), is("hadoop"));
BytesWritable是对二进制数据数组的封装。他的序列化格式为一个指定所含数据字节数的整数域(4字节),后跟数据内容本身。例如,长度为2 的字节数组包含数据值3和5,序列化形式为一个4字节的整数(00000002)和该数组中的两个字节(03和05):
BytesWritable b = new BytesWritable(new byte[]{3, 5});
byte[] bytes = serialize(b);
assertThat(StringUtils.byteToHexString(bytes), is("000000020305"));
BytesWritable是可变的,其值可以通过set()方法进行修改。和Text相似,BytesWritable类的getBytes()方法返回的字节数组长度(容量)可能无法体现BytesWritable所存储数据的实际大小。可以通过getLength()方法来确定BytesWritable的大小。示例如下:
b.setCapacity(11);
assertThat(b.getLength(), is(2));
assertThat(b.getBytes().length, is(11));
NullWritable是writable的特殊类型,他的序列化长度为0。他并不从数据流中读取数据,也不写入数据。他充当占位符;例如,在MapReduce中,如果不需要使用键或值的序列化地址,就可以将键或值声明为NullWritable,这样可以高效存储常量空值。如果希望存储一系列数值,与键-值对相对,NullWritable也可以用作在SequenceFile中的键。他是一个不可变的单实例类型,通过调用NullWritable.get()方法可以获取这个实例。
ObjectWritable是对Java基本类型(String,enum,Writable,null或这些类型组成的数组)的一个通用封装。他在Hadoop RPC中用于对方法的参数和返回类型进行封装和解封装。
当一个字段中包含多个类型时,ObjectWritable非常有用:例如,如果SequenceFile中的值包含多个类型,就可以将值类型声明为ObjectWritable,并将每个类型封装在一个ObjectWritable中。作为一个通用的机制,每次序列化都写封装类型的名称,这非常浪费空间。如果封装的类型数量比较少并且能够提前知道,那么可以通过使用静态类型的数组,并使用对序列化后的类型的引用加入位置索引来提高性能。GenericWritable类采取的就是这种方式,所以你得在继承的子类中指定支持什么类型。
org.apache.hadoop.io软件包中共有6个Writable集合类,分别是ArrayWritable、ArrayPrimitiveWritable、TwoDArrayWritable、MapWritable、SortedMapWritable以及EnumMapWritable。
ArrayWritable和TwoDArrayWritable是对Writable的数组和二维数组(数组的数组)的实现。ArrayWritable或TwoDArrayWritable中所有元素必须是同一类的实例(在构造函数中指定),如下所示:
ArrayWritable writable = new ArrayWritable(Text.class);
如果Writable根据类型来定义,例如SequenceFile的键或值,或更多时候作为MapReduce的输入,则需要继承ArrayWritable(或相应的TwoDArrayWritable类)并设置静态类型。示例如下:
public class TextArrayWritable extends ArrayWritable {
public TextArrayWritable() {
super(text.class);
}
}
ArrayWritable和TwoDArrayWritable都有get()、set()和toArray()方法。toArray()方法用于新建该数组(或二维数组)的一个“浅拷贝”(shallow copy)。
ArrayPrimitiveWritable是对Java基本数组类型的一个封装。调用set()方法时,可以识别相应组件类型,因此无需通过继承该类来设置类型。
MapWritable和SortedMapWritable分别实现了java.util.Map
MapWritable src = new MapWritable();
src.put(new IntWritable(1), new Text("cat"));
src.put(new VIntWritable(2), new LongWritable(163));
MapWritable dest = new MapWritable();
WritableUtils.cloneInto(dest, src);
assertThat((Text) dest.get(new IntWritable(1)), is(new Text("cat")));
assertThat((LongWritable) dest.get(new VIntWritable(2)));
is(new LongWritable(163));
显然,可以通过Writable集合类来实现集合和列表。可以使用MapWritable类型(或针对排序集合,使用SortedMapWritable类型)来枚举集合中的元素,用NullWritable类型枚举值。对集合的枚举类型可采用EnumSetWritable。对于单类型的Writable列表,使用ArrayWritable就足够了,但如果现需要把不同的Writable类型存储在单个列表中,可以用GenericWritable将元素封装在一个ArrayWritable中。另一个可选方案是,可以借鉴MapWritable的思路写一个通用的ListWritable。