LoginSignup
5

More than 3 years have passed since last update.

ByteBufferの複製には、バイトオーダーの再設定が必要

Posted at

バイト列を扱う際、バイトオーダーを意識しないといけない場合があります。

このような場合、Javaでは、ByteBufferクラスを使用すると便利です。ByteBufferにはバイトオーダーを設定でき、複数バイトから構成される値を簡単に扱えるためです。例えば、次のコードでは、リトルエンディアンの値を扱っています。

LittleEndianSample.java
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class LittleEndianSample {
    public static final void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(Short.BYTES);

        // リトルエンディアンの16ビット整数。下位バイトが0x11、上位バイトが0x22。
        buffer.order(ByteOrder.LITTLE_ENDIAN)
            .put((byte) 0x11)
            .put((byte) 0x22)
            .rewind();

        System.out.printf("0x%x\n", buffer.getShort());
    }
}

実行結果は次の通りです。

0x2211

ところが、上記のByteBufferを複製すると、バイトオーダーが想定外のものになってしまいます。例えば、リトルエンディアンのByteBufferを返すメソッドを書いているとします。その呼び出し元がByteBufferの内容を変更できないように、読み取り専用で返すことにします。しかし、次のようなコードでは、呼び出し元がビッグエンディアンのByteBufferを受け取ってしまいます。

BadSample.java
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class BadSample {
    private static class BufferObject {
        private final ByteBuffer buffer;

        public BufferObject() {
            buffer = ByteBuffer.allocate(Short.BYTES);

            // リトルエンディアンの16ビット整数。下位バイトが0x11、上位バイトが0x22。
            buffer.order(ByteOrder.LITTLE_ENDIAN)
                .put((byte) 0x11)
                .put((byte) 0x22)
                .rewind();
        }

        private ByteBuffer getBuffer() {
            // 読み取り専用で複製(誤り。ビッグエンディアンになる)
            return buffer.asReadOnlyBuffer();
        }
    }

    public static final void main(String[] args) {
        BufferObject bufferObject = new BufferObject();

        ByteBuffer buffer = bufferObject.getBuffer();

        System.out.printf("0x%x\n", buffer.getShort());
    }
}

実行結果は次のようになります。上位バイトと下位バイトとが入れ替わり、ビッグエンディアンになっていることがわかります。

0x1122

なぜこのような挙動となるのでしょうか。

OpenJDKのIssue Trackerに投稿されたIssue JDK-7178109によると、この挙動は仕様とのことです。

新規に作成されたByteBufferのバイトオーダーは、常にビッグエンディアンとなります。つまり、allocate()やallocateDirect()以外にも、下記のメソッドから返されるByteBufferのバイトオーダーは、元のByteBufferのバイトオーダーにかかわらず、常にビッグエンディアンです。

  • duplicate()
  • asReadOnlyBuffer()
  • wrap()
  • slice()
  • alignedSlice()

Java SEでは、バージョン9のAPIドキュメントから、上記の各メソッドに、作成されたByteBufferのバイトオーダーがビッグエンディアンになることが記されています。しかし、AndroidのAPIドキュメントでは、現時点(2019/7/9)ではそのような記述はありません。

以上から、バイトオーダーを保ったままByteBufferを複製するには、複製後にorder()でバイトオーダーを再度設定する必要があります。

DuplicationSample.java
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class DuplicationSample {
    private static class BufferObject {
        private final ByteBuffer buffer;

        public BufferObject() {
            buffer = ByteBuffer.allocate(Short.BYTES);

            // リトルエンディアンの16ビット整数。下位バイトが0x11、上位バイトが0x22。
            buffer.order(ByteOrder.LITTLE_ENDIAN)
                .put((byte) 0x11)
                .put((byte) 0x22)
                .rewind();
        }

        private ByteBuffer getBuffer() {
            // 読み取り専用で複製。バイトオーダーを設定する必要がある。
            return buffer.asReadOnlyBuffer().order(buffer.order());
        }
    }

    public static final void main(String[] args) {
        BufferObject bufferObject = new BufferObject();

        ByteBuffer buffer = bufferObject.getBuffer();

        System.out.printf("0x%x\n", buffer.getShort());
    }
}

実行結果は次の通りです。バイトオーダーが正しくリトルエンディアンになりました。

0x2211

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
5