《Hadoop权威指南:大数据的存储与分析》—5.3.2 Writable类

举报
清华大学出版社 发表于 2019/10/12 18:51:15 2019/10/12
【摘要】 本节书摘来自清华大学出版社《Hadoop权威指南:大数据的存储与分析》一书中第五章,第5.3.2节,作者是Tom White , 王 海 华 东 刘 喻 吕粤海 译。

5.3.2  Writable

Hadoop自带的org.apache.hadoop.io包中有广泛的Writable类可供选择。它们的层次结构如图5-1所示。

image.png

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

19

double

DoubleWritable

8

 

对整数进行编码时,有两种选择,即定长格式(IntWritaleLongWritable)和变长格式(VIntWritableVLongWritable)。需要编码的数值如果相当小(-127127之间,包括-127127),变长格式就是只用一个字节进行编码;否则,使用第一个字节来表示数值的正负和后跟多少个字节。例如,值163需要两个字节:

byte[] data = serialize(new VIntWritable(163));

assertThat(StringUtils.byteToHexString(data), is("8fa3"));

 

如何在定长格式和变长格式之间进行选择呢?定长格式编码很适合数值在整个值域空间中分布非常均匀的情况,例如使用精心设计的哈希函数。然而,大多数数值变量的分布都不均匀,一般而言变长格式会更节省空间。变长编码的另一个优点是可以在VIntWritableVLongWritable转换,因为它们的编码实际上是一致的。所以选择变长格式之后,便有增长的空间,不必一开始就用8字节的long表示。

2. Text类型

Text是针对UTF-8序列的Writable类。一般可以认为它是java.lang.StringWritable等价。

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()方法,该方法类似于StringindexOf()方法:

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  一旦开始使用需要多个字节来编码的字符时,TextString之间的区别就昭然若揭了。考虑表5-8显示的Unicode字符。[1]

所有字符(除了表中最后一个字符U+10400),都可以使用单个Java char类型来表示。U+10400是一个候补字符,并且需要两个Java char来表示,称为字符代理对”(surrogate pair)。范例5-5中的测试显示了处理一个字符串(5-8中的由4个字符组成的字符串)StringText之间的差别。


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的另一个区别在于它是可变的(与所有HadoopWritable接口实现相似,NullWritable除外,它是单实例对象)。可以通过调用其中一个set()方法来重用Text实例。例如:

Text t = new Text("hadoop");

t.set("pig");

assertThat(t.getLength(), is(3));

assertThat(t.getBytes().length, is(3));

spacer.gif在某些情况下,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的字节数组包含数值35,序列化形式为一个4字节的整数(00000002)和该数组中的两个字节(0305)

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

NullWritableWritable的特殊类型,它的序列化长度为0。它并不从数据流中读取数据,也不写入数据。它充当占位符;例如,在MapReduce中,如果不需要使用键或值的序列化地址,就可以将键或值声明为NullWritable,这样可以高效存储常量空值。如果希望存储一系列数值,与键-值对相对,NullWritable也可以用作在SequenceFile中的键。它是一个不可变的单实例类型,通过调用NullWritable.get()方法可以获取这个实例。

5. ObjectWritableGenericWritable

ObjectWritable是对Java基本类型(StringenumWritablenull或这些类型组成的数组)的一个通用封装。它在Hadoop RPC中用于对方法的参数和返回类型进行封装和解封装。

当一个字段中包含多个类型时,ObjectWritable非常有用:例如,如果SequenceFile中的值包含多个类型,就可以将值类型声明为ObjectWritable,并将每个类型封装在一个ObjectWritable中。作为一个通用的机制,每次序列化都写封装类型的名称,这非常浪费空间。如果封装的类型数量比较少并且能够提前知道,那么可以通过使用静态类型的数组,并使用对序列化后的类型的引用加入位置索引来提高性能。GenericWritable类采取的就是这种方式,所以你得在继承的子类中指定支持什么类型。

6. Writable集合类

org.apache.hadoop.io软件包***有6Writable集合类,分别是ArrayWritableArrayPrimitiveWritableTwoDArrayWritableMapWritableSortedMapWritable以及EnumMapWritable

ArrayWritableTwoDArrayWritable是对Writable的数组和两维数组(数组的数组)的实现。ArrayWritableTwoDArrayWritable中所有元素必须是同一类的实例(在构造函数中指定),如下所示:

ArrayWritable writable = new ArrayWritable(Text.class);

 

如果Writable根据类型来定义,例如SequenceFile的键或值,或更多时候作为MapReduce的输入,则需要继承ArrayWritable(或相应的TwoDArray Writable)并设置静态类型。示例如下:

public class TextArrayWritable extends ArrayWritable {

  public TextArrayWritable() {

    super(Text.class);

  }

}

 

ArrayWritableTwoDArrayWritable都有get()set()toArray()方法。toArray()方法用于新建该数组(或二维数组)的一个浅拷贝(shallow copy)

ArrayPrimitiveWritable是对Java基本数组类型的一个封装。调用set()方法时,可以识别相应组件类型,因此无需通过继承该类来设置类型。

MapWritableSortedMapWritable分别实现了java.util.Map<WritableWritable>java.util.SortedMap<WritableComparable, Writable>。每个键/值字段使用的类型是相应字段序列化形式的一部分。类型存储为单个字节(充当类型数组的索引)。在org.apache.hadoop.io包中,数组经常与标准类型结合使用,而定制的Writable类型也通常结合使用,但对于非标准类型,则需要在包头指明所使用的数组类型。根据实现,MapWritable类和SortedMapWritable类通过正byte值来指示定制的类型,所以在MapWritableSortedMapWritable实例中最多可以使用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



【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。