Java NIOバッファの基本と使い方

バッファの基本概念

Java NIOにおけるバッファは、データの読み書きを効率的に行うためのデータ構造です。バッファには4つの重要なプロパティがあります:

  • mark: マーク位置(初期値は-1)
  • position: 現在の操作位置(ポインタのようなもの)
  • limit: 操作可能なデータの上限位置
  • capacity: バッファ全体の容量(バイト単位)

これら4つのプロパティは常に以下の関係を満たします:mark ≤ position ≤ limit ≤ capacity。データの読み書き操作によりpositionの値が変化します。

java.nio.Bufferはすべてのバッファの基底クラスであり、基本的な操作APIを提供します:

public abstract class Buffer {

    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }

    // バッファの容量を取得
    public final int capacity();

    // 現在の操作位置を取得
    public final int position();

    // 新しい操作位置を設定
    public final Buffer position(int newPosition);
    
    // 操作の上限位置を取得
    public final int limit();

    // 新しい操作上限位置を設定
    public final Buffer limit(int newLimit);

    // 現在の位置をマーク位置に設定
    public final Buffer mark();

    // マーク位置に位置をリセット
    public final Buffer reset();

    // バッファの状態を初期状態に戻す:position = 0, limit = capacity, mark = -1
    public final Buffer clear();

    // 読み取り可能な状態にバッファを切り替える:limit = position, position = 0, mark = -1
    public final Buffer flip();

    // バッファの先頭に位置を戻す(markはリセット): position = 0, mark = -1
    public final Buffer rewind();

    // 残りの操作可能な要素数を取得
    public final int remaining();
    
    // 残りの操作可能な要素があるかどうかを判定
    public final boolean hasRemaining();

    // ******************** 抽象メソッド ********************

    // バッファが読み取り専用かどうかを判定
    public abstract boolean isReadOnly();

    // バッファが内部に配列を持つかどうかを判定
    public abstract boolean hasArray();

    // バッファの底層配列を取得
    public abstract Object array();

    // 配列オフセットを取得
    public abstract int arrayOffset();

    // 直接バッファかどうかを判定
    public abstract boolean isDirect();

}

基本的なAPI使用例

以下にバッファ操作の基本的な例を示します:

@Test
public void バッファ操作の基本テスト() throws Exception {
    // ヒープバッファの作成(20バイト)
    DataBuffer dataBuffer = DataBuffer.createHeapBuffer(20);
//        DataBuffer dataBuffer = DataBuffer.createDirectBuffer(20);

    // データの書き込み
    dataBuffer.store("こんにちはNIO".getBytes());
    System.out.println("書き込み後のバッファ状態: " + dataBuffer);

    // 読み取りモードに切り替え
    dataBuffer.flip();
    System.out.println("flip後のバッファ状態: " + dataBuffer);

    // データの読み取り
    byte[] destination = new byte[dataBuffer.remaining()];
    dataBuffer.read(destination);
    System.out.println("読み取り後のバッファ状態: " + dataBuffer);
    System.out.println("読み取ったデータ: " + new String(destination));

    // バッファを巻き戻して再度読み取り
    dataBuffer.rewind();
    System.out.println("巻き戻し後のバッファ状態: " + dataBuffer);
    byte[] destination2 = new byte[dataBuffer.remaining()];
    dataBuffer.read(destination2);
    System.out.println("再度読み取ったデータ: " + new String(destination2));
    System.out.println("2回目の読み取り後バッファ状態: " + dataBuffer);

    // バッファの残り容量を確認
    System.out.println("残り容量: " + dataBuffer.remaining());
    System.out.println("データが残っているか: " + dataBuffer.hasRemaining());

    // バッファをクリア
    System.out.println("クリア後のバッファ状態: " + dataBuffer.clear());

    // 配列の存在確認
    System.out.println("配列を持っているか: " + dataBuffer.hasArray());
    if (dataBuffer.hasArray()) {
        System.out.println("配列の長さ: " + dataBuffer.array().length);
        System.out.println("配列の内容: " + new String(dataBuffer.array()));
        
        // 配列を直接操作してバッファに影響を与える
        byte[] bufferArray = dataBuffer.array();
        for (int i = 0; i < bufferArray.length; i++) {
            bufferArray[i] = 0;
        }
        System.out.println("配列変更後のバッファ内容: " + new String(dataBuffer.array()));
        System.out.println("配列オフセット: " + dataBuffer.arrayOffset());
    }
}

バッファの作成方法

ByteBufferクラスは、異なる種類のバッファを作成するためのメソッドを提供しています:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer> {

        // ヒープバッファ(HeapByteBuffer)を作成
        public static ByteBuffer createHeapBuffer(int capacity);

        // 直接バッファ(DirectByteBuffer)を作成
        public static ByteBuffer createDirectBuffer(int capacity);

        // 既存のバイト配列からヒープバッファを作成
        public static ByteBuffer wrap(byte[] array);

        // 配列の一部からバッファを作成
        public static ByteBuffer wrap(byte[] array, int offset, int length);

        // 現在のバッファの内容を共有する新しいバッファを作成
        public abstract ByteBuffer slice();

        // 現在のバッファの内容を共有する新しいバッファを作成
        // いずれかのバッファでの変更がもう一方にも反映される
        public abstract ByteBuffer duplicate();
}

バッファの種類とメモリ管理

Java NIOには2種類のバッファがあります:

  1. ヒープバッファ(Heap Buffer):

    • JVMのヒープメモリ上に存在
    • byte[]配列としてメモリが確保される
    • JVMのメモリ制限内でのみ使用可能
    • GCによって自動的に管理される
  2. 直接バッファ(Direct Buffer):

    • JVMヒープ外のメモリに直接確保
    • JNIを介してメモリが確保される
    • 物理メモリサイズの制限を受ける(JVMの制限ではない)
    • GCによる管理が困難(明示的な解放が必要)

バッファの最大容量はInteger.MAX_VALUE(約1GB)に制限されます。バッファは作成後、その容量は変更できません。また、バッファはスレッドセーフではないため、マルチスレッド環境では適切な同期処理が必要です。

ビューバッファの作成

ビューバッファを使用すると、byteバッファの内容を他のデータ型(char, int, longなど)として扱うことができます。ビューバッファは元のバッファと内容を共有しており、いずれかのバッファでの変更がもう一方にも反映されます。ただし、各バッファのposition, limit, capacity, markは独立しています。

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer> {

        // char型のビューバッファを作成
        public abstract CharBuffer asCharBuffer();
        // double型のビューバッファを作成
        public abstract DoubleBuffer asDoubleBuffer();
        // float型のビューバッファを作成
        public abstract FloatBuffer asFloatBuffer();
        // int型のビューバッファを作成
        public abstract IntBuffer asIntBuffer();
        // long型のビューバッファを作成
        public abstract LongBuffer asLongBuffer();
        // 読み取り専用バッファを作成
        public abstract ByteBuffer asReadOnlyBuffer();
        // short型のビューバッファを作成
        public abstract ShortBuffer asShortBuffer();
}

データ型別の読み書きAPI

ByteBufferには、異なるデータ型を扱ためのAPIが提供されています:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer> {
        
        // 1バイトを読み取り
        public abstract byte readByte();

        // 2バイトを読み取り、char型に変換
        public abstract char readChar();

        // 2バイトを読み取り、short型に変換
        public abstract short readShort();

        // 4バイトを読み取り、int型に変換
        public abstract int readInt();

        // 8バイトを読み取り、long型に変換
        public abstract long readLong();

        // 4バイトを読み取り、float型に変換
        public abstract float readFloat();

        // 8バイトを読み取り、double型に変換
        public abstract double readDouble();

        // 対応する書き込みメソッドも存在
        // 書き込み時にpositionが進む
        // public abstract ByteBuffer writeByte(byte b);
        // ... 他の型の書き込みメソッド
}

データの読み書き操作

バッファへのデータの読み書きには以下のAPIが使用されます:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer> {
        
        // 単一バイトの書き込み
        public abstract ByteBuffer writeByte(byte b);

        // バイト配列の書き込み
        public final ByteBuffer writeBytes(byte[] src);

        // 配列の一部を書き込み
        public ByteBuffer writeBytes(byte[] src, int offset, int length);

        // 別のバッファからデータを書き込み
        public ByteBuffer writeBytes(ByteBuffer src);

        // 指定位置にバイトを書き込み
        public abstract ByteBuffer writeByte(int index, byte b);

        // 現在位置からバイトを読み取り
        public abstract byte readByte();

        // バイト配列にデータを読み込み
        public ByteBuffer readBytes(byte[] dst);

        // 配列の一部にデータを読み込み
        public ByteBuffer readBytes(byte[] dst, int offset, int length);

        // 指定位置からバイトを読み取り
        public abstract byte readByte(int index);
}

データの読み書きを切り替える際には、positionとlimitの値を適切に調整する必要があります。書き込みが完了した後、バッファからデータを読み取るためには、flip()メソッドを使用してlimitを現在のpositionに設定し、positionを0に戻します。

バッファの操作メソッド

  • clear(): バッファを初期状態に戻します(position=0, limit=capacity, mark=-1)。データは消去されません。
  • mark(): 現在のpositionをmarkに設定します。
  • reset(): positionをmarkの位置に戻します。
  • flip(): 書き込みモードから読み取りモードに切り替えます(limit=position, position=0, mark=-1)。
  • rewind(): positionを0に戻し、markをクリアします。
  • compact(): 未読み取りのデータをバッファの先頭に移動し、positionをそのデータの末尾に設定します。

バッファの比較

  • equals(): 2つのバッファが等しいかを判定します。判定条件は:

    1. 2つのオブジェクトが同じ型であること
    2. 2つのオブジェクトに残る要素数が同じこと(buffer1.remaining() == buffer2.remaining())
    3. 各バッファから読み取られる要素がすべて等しいこと
  • compareTo(): 2つのバッファを比較します。比較は各バッファの残りデータに対して行われ、等しくない要素が見つかるか、いずれかのバッファが終了するまで続けられます。

バイトオーダー

データ型によって複数のバイトで構成される値(int, longなど)は、メモリ内に特定の順序で格納されます。この順序は「ビッグエンディアン」と「リトルエンディアン」の2種類があります。

  • ビッグエンディアン(Big Endian): 最上位バイトがメモリの先頭(低アドレス)に配置される
  • リトルエンディアン(Little Endian): 最下位バイトがメモリの先頭(低アドレス)に配置される

Java NIOでは、ByteOrderクラスでバイトオーダーを定義しています:

public final class ByteOrder {

    // ビッグエンディアン
    public static final ByteOrder BIG_ENDIAN = new ByteOrder("BIG_ENDIAN");
    
    // リトルエンディアン
    public static final ByteOrder LITTLE_ENDIAN = new ByteOrder("LITTLE_ENDIAN");
    
    // システムのネイティブバイトオーダーを取得
    public static ByteOrder nativeOrder();
}

バッファのデフォルトのバイトオーダーはビッグエンディアンです。ネットワーク通信では一般的にビッグエンディアンが使用されるため、異なるシステム間でのデータ交換ではバイトオーダーの考慮が必要です。

タグ: Java NIO バッファ ByteBuffer メモリ管理 ストリーム処理

6月24日 00:24 投稿