BytesWritable#getBytesを使うときの注意点

ちょっとマニアックなネタですが最近遭遇したのでメモっておきます。

サンプルコードはこちら

		byte[] b = new byte[]{1,2};
		
		BytesWritable byteWritable1 = new BytesWritable();
		byteWritable1.set(b, 0, b.length);
		assertThat(byteWritable1.getLength(), is(2));
		assertThat(byteWritable1.getCapacity(), is(3));
		assertThat(byteWritable1.getBytes().length, is(3));

最後の2行のところで何故2ではなく3なんだ?って話です。

BytesWritable#set使うとこうなります。

BytesWritableのコンストラクタ使えば予想通りに2になります。

		BytesWritable byteWritable2 = new BytesWritable(b);
		assertThat(byteWritable2.getLength(), is(2));
		assertThat(byteWritable2.getCapacity(), is(2));
		assertThat(byteWritable2.getBytes().length, is(2));

これはですね、BytesWritable#set使うとsetSizeが呼ばれるんですが、そこでバイト配列の容量をチェックしていて足りなかったら1.5倍に増やしているからです。

  public void setSize(int size) {
    if (size > getCapacity()) {
      setCapacity(size * 3 / 2);
    }
    this.size = size;
  }

つまり、[1, 2]というバイト配列だと[1,2,0]になります。BytesWritable#getBytesで取れるバイト配列は[1,2,0]です。
getCapacityで取れるのはバイト配列の長さなので3になります。
一方でBytesWritable#getLengthで取れるのはバイト配列の真のデータ容量の大きさなので2になります。
BytesWritableのコンストラクタを使う場合はsetSizeが呼ばれないのでgetLengthもgetCapacityも同じになります。

というわけで、0が後ろにくっついたデータじゃなくて正味のデータを取得する場合はgetBytesじゃなくて以下のような感じにする必要がありますね。

byte[] data = new byte[byteWritable1.getLength()];
System.arraycopy(byteWritable1.getBytes(), 0, data, 0, byteWritable1.getLength());

まあBytesWritableにどのようにデータをセットしたかにもよるわけですが、例えばHiveのbinary型を扱っているLazyBinaryクラスだと以下のようにBytesWritable#setを使っているので注意が必要です。

  public void init(ByteArrayRef bytes, int start, int length) {

    byte[] recv = new byte[length];
    System.arraycopy(bytes.getData(), start, recv, 0, length);
    boolean arrayByteBase64 = Base64.isArrayByteBase64(recv);
    if (arrayByteBase64) {
      LOG.debug("Data not contains valid characters within the Base64 alphabet so " +
                "decoded the data.");
    }
    byte[] decoded = arrayByteBase64 ? Base64.decodeBase64(recv) : recv;
    data.set(decoded, 0, decoded.length);
  }

この辺は混乱しがちなのでHadoopのJIRAにも上がっているようですね。
https://issues.apache.org/jira/browse/HADOOP-6298

ちなみに象本 4.3.2.3 BytesWritable の説明のところで

バイト配列の大きさは、BytesWritableのgetBytes()メソッドが返してくれます。これはバイト配列の容量であり、BytesWritableに格納されたデータの実際の大きさを反映しているとは限りません。BytesWritableの大きさを特定するにはgetLength()を呼びます。

ってありますが、これはおそらくgetBytes()メソッドじゃなくてgetCapacity()メソッドだと思います。バイト配列の大きさとか容量とか言ってますしね。原書第3版を見てもgetBytes()になっているので翻訳というより原書の誤植な気がしますけど。