整套大数据学习资料(视频+笔记)百度网盘无门槛下载:http://www.edu360.cn/news/content?id=3377

4.3.2. Writable类

hadoop 花牛 7℃ 0评论

Hadoop自带的org.apache.hadoop.io包中有广泛的Writable类可供选

择。它们形成如图4-1所示的层次结构。

 blob.png

图4-1. Writable类的层次结构

Java基本类型的Writable封装器

Writable类对Java基本类型(参见表4-7)提供封装,char除外(可以存储在IntWritable中)。所有的封装包含get()和set()两个方法用于读取或存储封装的值。

表4-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表示。

Text类型

Text是针对UTF-8序列的Writable类。一般可以认为它等价于java.lang.String的 Writable。Text 替代了 UTF8 类,但这并不是一个很好的替代,一因为不支持对字节数超过32767的字符串进行编码,二者因为它使用的是JavaUTF-8修订版。

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(-l));

注意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));

Unicode  —旦开始使用需要多个字节来编码的字符时,TextString之间的区别就昭然若揭了。考虑表4-8显示的Unicode字符。

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

 4-8. Unicode 字符

Unicode

编码点

U+0041

U+00DF

U+6771

U+10400

名称

拉丁大写字母A

拉丁小写字母 SHARP S

无(统一表示 的汉字)

DESERET CAPITAL LETTER LONG I

UTF-8

41

c39f

e69dbl

F0909080

编码单元

Java表示

\u0041

\u00DF

\u6771

\uuD801\uDC00

 

范例4-5.验证String和Text的差异性的测试

public class StringTextComparisonTest {
    @Test
    public void string() throws UnsupportedEncodingException {
        String s = "\u0041\u00DF\u6771\uD801\uDC00H; assertThat(s.length(),is(5));
        assertThat(s.getBytes("UTF-8").length, is(10));
        assertThat(s.indexOf("\u0041"), is(0));
        assertThat(s.indexOf("\u00DF"), is(l));
        assertThat(s.index0f("\u6771"), is(2));
        assertThat(s.indexOf("\uD801\uDC00"), is(3));
        assertThat(s.charAt(0), is("\u0041"));
        assertThat(s.charAt(l), 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(l),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字符进行迭代是非常复杂的,因为无法通过简单地增加索引值来实现该迭代。同时迭代的语法有些模糊(参见范例4-6):将Text对象转换为java.nio.ByteBuffer对象,然后利用缓冲区对Text对象反复调用bytesToCodePoint()静态方法。该方法能够获取下一代码的位置,并返回相应的int值,最后更新缓冲区中的位置。当bytesToCodePoint()返回-1 时,则检测到字符串的末尾。

范例4-6.遍历Text对象中的字符

public class Textlterator {
    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))!=-l){
            System.out.println(Integer.toHexString(cp));
        }
    }
}

运行这个程序,打印出字符串中四个字符的编码点(code point):

% hadoop Textlterator

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类并不像javalang.String类那样有丰富的字符串操作API。所以,在多数情况下需要将Text对象转换成String对 象。这一转换通常通过调用toString()方法来实现:

assertThat(new Text("hadoop").toString(), is("hadoop"));

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()方法返回的字节数组长度一一容量——可能无法体现BytesWitable所存储数据的实际大小。可以通过getLength()方法来确定BytesWritable的大小。示例如下:

b.setCapacity(11);
assertThat(b.getLength(), is(2));
assertThat(b.getBytes().length, is(11));

NullWritable

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

ObjectWritable和 GenericWritable

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

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

Writable集合类

org.apache.hadoop.io 软件包中一共有 6 个 Writable 集合类:ArrayWritable ArnayPrimitiveWritable TwoDArrayWritable MapWritable、 SortedMapWritable 和 EnumMapWritable

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

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<WritableWritable>和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

转载请注明:全栈大数据 » 4.3.2. Writable类

喜欢 (0)or分享 (0)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址