LoginSignup
3
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
3
5
0

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
  3. You can use dark theme
What you can do with signing up
3
5