Hadoop的序列化(上)--数值类型

序列化(serialization),是指将结构化对象转换为字节流以便网络传输或存储至磁盘,反序列化(deserialization)就是把字节流转换为结构化对象的过程.

Hadoop为什么需要这个过程,因为在大型集群中,网络带宽资源是非常宝贵的,能省则省.

在Hadoop中,系统中的多个节点上进程间的通信是通过"远程过程调用"(RPC)是限定.RPC协议将消息序列化成二进制流之后发送到远程节点,远程节点接着将二进制流反序列化为原始消息.

说到这里大家可能会想到,Java中也有序列化接口,而且Hadoop就是用Java实现的,为什么不直接使用Java的序列化接口 java.io.Serializable呢,Hadoop之父,Doug Cutting是这么说的:"因为它看起来太复杂,然而我们需要有一个至精至简的机制,可以精确控制对象的读和写,这个机制将是Hadoop的核心."

Hadoop使用的是自己的序列化格式Writable,它绝对紧凑,速度快,但不太容易用Java以外的语言进行扩展或使用.

Writable是一个接口,Hadoop的序列化都是从这个接口扩展而来的提供了两个方法:,write()用来写入DataOutput二进制流,readFields()用来从DataInput二进制流读取状态:

public interface Writable {
  void write(DataOutput out) throws IOException;
  void readFields(DataInput in) throws IOException;
}

 接下来看看Hadoop中具体的序列化类,首先来看看IntWritable,对应到Java就是int类型.我们要做的事情是,先给它序列化看看变成了什么内容,再反序列化回去看看能不能恢复原本的内容.

首先写一个工具类:

import org.apache.hadoop.io.*;

import java.io.*;
import java.util.Arrays;

/**
 * 工具类
 */
class Utils {

    /**
     * 将传入的writable对象序列化为字节数组.
     */
    static byte[] serialize(Writable writable) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DataOutputStream dataOut = new DataOutputStream(out);
        writable.write(dataOut);
        dataOut.close();
        return out.toByteArray();
    }

    /**
     * 将字节数组反序列化为writable对象.
     */
    static byte[] deserialize(Writable writable, byte[] bytes) throws IOException{
        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        DataInputStream dataIn = new DataInputStream(in);
        writable.readFields(dataIn);
        dataIn.close();
        return bytes;
    }

    /**
     * 输出一个字节数组.
     */
    static void printArray(byte[] bytes) {
        System.out.println(Arrays.toString(bytes));
    }

    /**
     * 比较两个参数是否一样,如果为false,就分别输出两个参数.
     */
    static void assertThat(Object o1, Object o2){
        if (o1 != null && o2 != null) {
            if (o1.equals(o2)) {
                System.out.println(true);
            } else {
                System.out.print(false);
                System.out.println(",(" + o1.toString() + "," + o2.toString() + ")");
            }
        }
    }
}

序列化与反序列化方法参考的是<权威指南>的写法,核心是一个字节数组.

定长类型

首先创建一个IntWritable对象, 然后利用工具类的方法将其序列化并输出:

    IntWritable iw1 = new IntWritable(100);
    byte[] b1 = Utils.serialize(iw1);
    Utils.printArray(b1);

得到的结果为:

[0, 0, 0, 100]

100比较容易知道是什么意思,但是前面3个0是啥意思啊?我们接着用更大的值来看看:

        IntWritable iw1 = new IntWritable(100);
        IntWritable iw2 = new IntWritable(127);
        IntWritable iw3 = new IntWritable(128);
        IntWritable iw4 = new IntWritable(255);
        IntWritable iw5 = new IntWritable(256);
        IntWritable iw6 = new IntWritable(512);
        IntWritable iw7 = new IntWritable(65536);
                //65536*256 = 16777216
        IntWritable iw8 = new IntWritable(16777216);

        //为了优化阅读效果,后面就不再写这条语句了.
        Utils.printArray(Utils.serialize(iw1));

再次运行,看到结果的时候其实应该就明白了:

[0, 0, 0, 100]
[0, 0, 0, 127]
[0, 0, 0, -128]
[0, 0, 0, -1]
[0, 0, 1, 0]
[0, 0, 2, 0]
[0, 1, 0, 0]
[1, 0, 0, 0]

 127对应[0, 0, 0, 127],而128却对应[0, 0, 0, -128],想到了什么?对了,封闭式运算:

Byte.MAX_VALUE + 1 == Byte.MIN_VALUE

byte的取值范围是127~-128.再来看看255与256,255对应的是[0, 0, 0, -1],256对应的是[0, 0, 1, 0],可以发现,在256的时候,第3个数字变成了1.再看看512,输出结果为[0, 0, 2, 0],65536的输出结果为[0, 1, 0, 0],16777216(=65535*256)的输出结果为[1, 0, 0, 0].怎么解释?

我们知道二进制运算时,比如一个二进制数是10010111,对应的十进制是151,一个运算方法是:

1*2^7 + 0*2^6 + 0*2^5+ 1*2^4+ 0*2^3+ 1*2^2+ 1*2^1+ 1*2^0

 如果把刚才的序列化结果理解为一个"256进制运算",是不是一切就可以解释的通?只不过要把负的byte类型数值转换为正的.比如我们来解释一下512,65536与1677216:

512
[   0   ,     0    ,    2   ,     0  ]
    0*256^3 + 0*256^2 + 2*256^1 + 0^256^0

65536:
[   0   ,     1    ,    0   ,     0  ]
    0*256^3 + 1*256^2 + 0*256^1 + 0^256^0

1677216
[   1   ,     0    ,    0   ,     0  ]
    1*256^3 + 0*256^2 + 0*256^1 + 0^256^0

结果完全正确,那么负数呢?

        IntWritable iw9 = new IntWritable(-256);
        IntWritable iw10 = new IntWritable(-512);
        IntWritable iw11 = new IntWritable(-65536);
        IntWritable iw12 = new IntWritable(-16777216);
        IntWritable iw13 = new IntWritable(-127);
        IntWritable iw14 = new IntWritable(-129);

运行结果:

[-1, -1, -1, 0]
[-1, -1, -2, 0]
[-1, -1, 0, 0]
[-1, 0, 0, 0]
[-1, -1, -1, -127]
[-1, -1, -1, 127]

 思路是一样的,只不过是反过来的封闭式运算,且计算的初始值从0变为-1:

Byte.MIN_VALUE - 1 == Byte.MAX_VALUE

还有LongWritable,具体原理是跟IntWritable一样的,只不过数组元素数扩充为8,IntWritable是4个数字代表int是4字节长度的,而LongWritable是8个数字也就代表了long是8字节长度.这里不做展示.

当然还有浮点型FloatWritable与DoubleWritable,具体的原理大家来分析一下,我不是太理解:

        FloatWritable fw1 = new FloatWritable(0.5F);
        FloatWritable fw2 = new FloatWritable(0.25F);
        FloatWritable fw3 = new FloatWritable(0.125F);
        DoubleWritable dw1 = new DoubleWritable(0.5);
        DoubleWritable dw2 = new DoubleWritable(0.25);
        DoubleWritable dw3 = new DoubleWritable(0.125);

运行结果: 

[63, 0, 0, 0]
[62, -128, 0, 0]
[62, 0, 0, 0]
[63, -32, 0, 0, 0, 0, 0, 0]
[63, -48, 0, 0, 0, 0, 0, 0]
[63, -64, 0, 0, 0, 0, 0, 0]

变长类型

Hadoop中还有一种变长类型:VIntWritable,VLongWritable.

对于可变与不可变的具体区别,在于序列化后的数组元素数量:

类型序列化后字节数组元素数
IntWritable 4
VIntWritable1~5
LongWritable 8
VLongWritable1~9

来看几个例子,看看最大元素数多出来的一个是什么.

        VIntWritable viw1 = new VIntWritable(100);
        VIntWritable viw2 = new VIntWritable(127);
        VIntWritable viw3 = new VIntWritable(128);
        VIntWritable viw4 = new VIntWritable(256);
        VIntWritable viw5 = new VIntWritable(512);
        VIntWritable viw6 = new VIntWritable(65536);
        VIntWritable viw7 = new VIntWritable(16777216);
        VIntWritable viw8 = new VIntWritable(-100);
        VIntWritable viw9 = new VIntWritable(-127);
        VIntWritable viw10 = new VIntWritable(-128);
        VIntWritable viw11 = new VIntWritable(-256);
        VIntWritable viw12 = new VIntWritable(-512);
        VIntWritable viw13 = new VIntWritable(-65536);
        VIntWritable viw14 = new VIntWritable(-16777216);

运行结果:

[100]
[127]
[-113, -128]
[-114, 1, 0]
[-114, 2, 0]
[-115, 1, 0, 0]
[-116, 1, 0, 0, 0]
[-100]
[-121, 126]
[-121, 127]
[-121, -1]
[-122, 1, -1]
[-122, -1, -1]
[-123, -1, -1, -1]

可以看到,正数情况下前面的0都不见了,只保留了后面有效的数字;负数情况下,后面的0都不见了,只保留了前面的有效数字.其他都是一样的,但是负数的情况还有不一样,下面来看看:

        for (int i = 0; i < Byte.MAX_VALUE * 2; i++) {
            byte[] b = Utils.serialize(new VIntWritable(- i -110));
            System.out.println("(" + ( -i - 110 ) + ","  + Utils.arrayToString(b) + ")");
        }

这里把输出做了一点处理,格式是:(数字(-i - 110),[字节数组]) ,下面来看看部分输出结果(注意箭头指的位置):

(-110,[-110])
(-111,[-111])
(-112,[-112])         ←
(-113,[-121, 112])    ←
(-114,[-121, 113])
(-115,[-121, 114])
    .
    .
    .
(-127,[-121, 126])
(-128,[-121, 127])
(-129,[-121, -128])
    .
    .
    .
(-254,[-121, -3])
(-255,[-121, -2])
(-256,[-121, -1])
(-257,[-122, 1, 0])
(-258,[-122, 1, 1])
(-259,[-122, 1, 2])
(-260,[-122, 1, 3])
    .
    .
    .

    

经过测试,只有-112~-113~-128会出现这种情况,具体原理不明... 

第一个数字在正数情况下是在-113~-116,每多一个数字就-1,负数经测试范围在-121~-123之间,多一个数字就-1,不知道代表什么意思...猜测第一个数字的最大值-113可能与-112到-113的变化有关系.当然也只是猜测.

下一篇说说非数值类型的序列化与反序列化.

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值