《Hadoop权威指南:大数据的存储与分析》—5.3.2 Writable类
5.3.2 Writable类
Hadoop自带的org.apache.hadoop.io包中有广泛的Writable类可供选择。它们的层次结构如图5-1所示。
图5-1. Writable类的层次结构
1. Java基本类型的Writable封装器
Writable类对所有Java基本类型(参见表5-7)提供封装,char类型除外(可以存储在IntWritable中)。所有的封装包含get()和set()两个方法用于读取或存储封装的值。
表5-7. Java基本类型的Writable类
Java基本类型 | Writable实现 | 序列化大小(字节) |
boolean | BooleanWritable | 1 |
byte | ByteWritable | 1 |
Short | ShortWritable | 2 |
int | IntWritable | 4 |
VintWritable | 1~5 | |
float | FloatWritable | 4 |
long | LongWritable | 8 |
VlongWritable | 1~9 | |
double | DoubleWritable | 8 |
对整数进行编码时,有两种选择,即定长格式(IntWritale和LongWritable)和变长格式(VIntWritable和VLongWritable)。需要编码的数值如果相当小(在-127和127之间,包括-127和127),变长格式就是只用一个字节进行编码;否则,使用第一个字节来表示数值的正负和后跟多少个字节。例如,值163需要两个字节:
byte[] data = serialize(new VIntWritable(163));
assertThat(StringUtils.byteToHexString(data), is("8fa3"));
如何在定长格式和变长格式之间进行选择呢?定长格式编码很适合数值在整个值域空间中分布非常均匀的情况,例如使用精心设计的哈希函数。然而,大多数数值变量的分布都不均匀,一般而言变长格式会更节省空间。变长编码的另一个优点是可以在VIntWritable和VLongWritable转换,因为它们的编码实际上是一致的。所以选择变长格式之后,便有增长的空间,不必一开始就用8字节的long表示。
2. Text类型
Text是针对UTF-8序列的Writable类。一般可以认为它是java.lang.String的Writable等价。
Text类使用整型(通过边长编码的方式)来存储字符串编码中所需的字节数,因此该最大值为2 GB。另外,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));
注意:harAt()方法返回的是一个表示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));
Unicode 一旦开始使用需要多个字节来编码的字符时,Text和String之间的区别就昭然若揭了。考虑表5-8显示的Unicode字符。[1]
所有字符(除了表中最后一个字符U+10400),都可以使用单个Java char类型来表示。U+10400是一个候补字符,并且需要两个Java char来表示,称为“字符代理对”(surrogate pair)。范例5-5中的测试显示了处理一个字符串(表5-8中的由4个字符组成的字符串)时String和Text之间的差别。
表5-8. Unicode字符
Unicode | U+0041 | U+00DF | U+6771 | U+10400 |
名称 | 拉丁大写字母A | 拉丁小写字母SHARP S | 无(统一表示的汉字) | DESERET CAPITAL LETTER LONG I |
UTF-8 | 41 | c39f | e69db1 | F0909080 |
Java表示 | \u0041 | \u00DF | \u6771 | \uuD801\uDC00 |
范例5-5. 验证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 text() {
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字符进行迭代是非常复杂的,因为无法通过简单地增加索引值来实现该迭代。同时迭代的语法有些模糊(参见范例5-6):将Text对象转换为java.nio.ByteBuffer对象,然后利用缓冲区对Text对象反复调用bytesToCodePoint()静态方法。该方法能够获取下一代码的位置,并返回相应的int值,最后更新缓冲区中的位置。当bytesToCodePoint()返回-1时,则检测到字符串的末尾。
范例5-6. 遍历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.getBytes().length, is(6));
以上代码说明了在调用getBytes()之前为什么始终都要调用getLength()方法,因为可以由此知道字节数组中多少字符是有效的。
对String重新排序 Text类并不像java.lang.String类那样有丰富的字符串操作API。所以,在多数情况下需要将Text对象转换成String对象。这一转换通常通过调用toString()方法来实现:
assertThat(new Text("hadoop").toString(), is("hadoop"));
3. BytesWritable
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));
4. NullWritable
NullWritable是Writable的特殊类型,它的序列化长度为0。它并不从数据流中读取数据,也不写入数据。它充当占位符;例如,在MapReduce中,如果不需要使用键或值的序列化地址,就可以将键或值声明为NullWritable,这样可以高效存储常量空值。如果希望存储一系列数值,与键-值对相对,NullWritable也可以用作在SequenceFile中的键。它是一个不可变的单实例类型,通过调用NullWritable.get()方法可以获取这个实例。
5. ObjectWritable和GenericWritable
ObjectWritable是对Java基本类型(String,enum,Writable,null或这些类型组成的数组)的一个通用封装。它在Hadoop RPC中用于对方法的参数和返回类型进行封装和解封装。
当一个字段中包含多个类型时,ObjectWritable非常有用:例如,如果SequenceFile中的值包含多个类型,就可以将值类型声明为ObjectWritable,并将每个类型封装在一个ObjectWritable中。作为一个通用的机制,每次序列化都写封装类型的名称,这非常浪费空间。如果封装的类型数量比较少并且能够提前知道,那么可以通过使用静态类型的数组,并使用对序列化后的类型的引用加入位置索引来提高性能。GenericWritable类采取的就是这种方式,所以你得在继承的子类中指定支持什么类型。
6. Writable集合类
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(或相应的TwoDArray Writable类)并设置静态类型。示例如下:
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<Writable,Writable>和java.util.SortedMap<WritableComparable, Writable>。每个键/值字段使用的类型是相应字段序列化形式的一部分。类型存储为单个字节(充当类型数组的索引)。在org.apache.hadoop.io包中,数组经常与标准类型结合使用,而定制的Writable类型也通常结合使用,但对于非标准类型,则需要在包头指明所使用的数组类型。根据实现,MapWritable类和SortedMapWritable类通过正byte值来指示定制的类型,所以在MapWritable和SortedMapWritable实例中最多可以使用127个不同的非标准Wirtable类。下面显示使用了不同键值类型的MapWritable实例:
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。
- 点赞
- 收藏
- 关注作者
评论(0)