1. 概要

Buffer クラスは、JavaNIOが構築される基盤です。 ただし、これらのクラスでは、ByteBufferクラスが最も優先されます。 これは、byteタイプが最も用途が広いためです。 たとえば、バイトを使用して、JVMで他の非ブールプリミティブ型を構成できます。 また、バイトを使用して、JVMと外部I/Oデバイス間でデータを転送できます。

このチュートリアルでは、ByteBufferクラスのさまざまな側面を調べます。

2. ByteBuffer作成

ByteBuffer は抽象クラスであるため、newインスタンスを直接構築することはできません。 ただし、インスタンスの作成を容易にする静的ファクトリメソッドを提供します。 簡単に言うと、 ByteBuffer インスタンスを作成するには、割り当てまたはラッピングの2つの方法があります。

2.1. 割り当て

割り当てにより、インスタンスが作成され、特定の容量のプライベートスペースが割り当てられます。 正確には、ByteBufferクラスにはallocateallocateDirectの2つの割り当てメソッドがあります。

alllocate メソッドを使用して、非直接バッファー、つまり、基になるbyte配列を持つバッファーインスタンスを取得します。

ByteBuffer buffer = ByteBuffer.allocate(10);

alllocateDirect メソッドを使用すると、直接バッファーが生成されます。

ByteBuffer buffer = ByteBuffer.allocateDirect(10);

簡単にするために、非直接バッファに焦点を当て、直接バッファの説明は後で説明します。

2.2. ラッピング

ラッピングにより、インスタンスは既存のbyte配列を再利用できます。

byte[] bytes = new byte[10];
ByteBuffer buffer = ByteBuffer.wrap(bytes);

そして、上記のコードは次のものと同等です。

ByteBuffer buffer = ByteBuffer.wrap(bytes, 0, bytes.length);

既存のbyte配列のデータ要素に加えられた変更は、バッファインスタンスに反映され、その逆も同様です。

2.3. オニオンモデル

これで、ByteBufferインスタンスを取得する方法がわかりました。 次に、 ByteBuffer クラスを3層のタマネギモデルとして扱い、層ごとに裏返しに理解してみましょう。

  • データとインデックスのレイヤー
  • データレイヤーの転送
  • ビューレイヤー

最内層では、 ByteBuffer クラスを、追加のインデックスを持つbyte配列のコンテナーと見なします。 中間層では、ByteBufferインスタンスを使用して他のデータ型との間でデータを転送することに重点を置いています。 最外層で異なるバッファベースのビューを使用して、同じ基になるデータを検査します。

3. ByteBufferインデックス

概念的には、 ByteBuffer クラスは、オブジェクト内にラップされたbyte配列です。 基になるデータとの間の読み取りまたは書き込み操作を容易にするための多くの便利な方法を提供します。 そして、これらの方法は、維持される指標に大きく依存しています。

ここで、 ByteBuffer クラスを、追加のインデックスを持つbyte配列のコンテナーに意図的に単純化してみましょう。

ByteBuffer = byte array + index

この概念を念頭に置いて、インデックス関連のメソッドを次の4つのカテゴリに分類できます。

  • 基本
  • マークしてリセット
  • クリア、フリップ、巻き戻し、コンパクト
  • 残る

3.1. 4つの基本的な指標

Bufferクラスには4つのインデックスが定義されています。 これらのインデックスは、基になるデータ要素の状態を記録します。

  • 容量:バッファーが保持できるデータ要素の最大数
  • 制限:読み取りまたは書き込みを停止するためのインデックス
  • 位置:読み取りまたは書き込みを行う現在のインデックス
  • マーク:記憶された位置

また、これらのインデックスの間には不変の関係があります。

0 <= mark <= position <= limit <= capacity

また、すべてのインデックス関連のメソッドはこれらの4つのインデックスを中心に展開していることに注意してください。

新しいByteBufferインスタンスを作成する場合、 mark は未定義であり、 position は0を保持し、limitは[ X153X]容量。 たとえば、ByteBufferに10個のデータ要素を割り当てましょう。

ByteBuffer buffer = ByteBuffer.allocate(10);

または、既存のバイト配列を10個のデータ要素でラップしましょう。

byte[] bytes = new byte[10];
ByteBuffer buffer = ByteBuffer.wrap(bytes);

その結果、マークは-1になり、位置は0になり、制限容量の両方が10になります。

int position = buffer.position(); // 0
int limit = buffer.limit();       // 10
int capacity = buffer.capacity(); // 10

容量は読み取り専用であり、変更できません。 ただし、 position(int)および limit(int)メソッドを使用して、対応するpositionおよびlimitを変更できます。

buffer.position(2);
buffer.limit(5);

そうすると、位置は2になり、制限は5になります。

3.2. マークしてリセット

mark()および reset()メソッドを使用すると、特定の位置を記憶して後で戻ることができます。

ByteBuffer インスタンスを最初に作成するとき、markは未定義です。 次に、 mark()メソッドを呼び出すことができ、markが現在の位置に設定されます。 いくつかの操作の後、 reset()メソッドを呼び出すと、positionmarkに戻ります。

ByteBuffer buffer = ByteBuffer.allocate(10); // mark = -1, position = 0
buffer.position(2);                          // mark = -1, position = 2
buffer.mark();                               // mark = 2,  position = 2
buffer.position(5);                          // mark = 2,  position = 5
buffer.reset();                              // mark = 2,  position = 2

注意点: mark が未定義の場合、 reset()メソッドを呼び出すと、InvalidMarkExceptionが発生します。

3.3. クリア、フリップ、巻き戻し、コンパクト

clear() flip() rewind()、および compact()メソッドには、いくつかの共通部分とわずかな違いがあります。

これらのメソッドを比較するために、コードスニペットを準備しましょう。

ByteBuffer buffer = ByteBuffer.allocate(10); // mark = -1, position = 0, limit = 10
buffer.position(2);                          // mark = -1, position = 2, limit = 10
buffer.mark();                               // mark = 2,  position = 2, limit = 10
buffer.position(5);                          // mark = 2,  position = 5, limit = 10
buffer.limit(8);                             // mark = 2,  position = 5, limit = 8

clear()メソッドは、 limitcapacityに、 position を0に、markに変更します。 ]から-1:

buffer.clear();                              // mark = -1, position = 0, limit = 10

flip()メソッドは、 limitpositionに、 position を0に、markに変更します。 ]から-1:

buffer.flip();                               // mark = -1, position = 0, limit = 5

rewind()メソッドは、 limit を変更せずに、 position を0に変更し、 markを-1に変更します。

buffer.rewind();                             // mark = -1, position = 0, limit = 8

compact()メソッドは、 limitcapacityに変更し、 position を残り( limit – positionに変更します。 ])、およびマークを-1に変更します。

buffer.compact();                            // mark = -1, position = 3, limit = 10

上記の4つの方法には、独自のユースケースがあります。

  • バッファを再利用するには、 clear()メソッドが便利です。 インデックスを初期状態に設定し、新しい書き込み操作の準備をします。
  • flip()メソッドを呼び出した後、バッファインスタンスは書き込みモードから読み取りモードに切り替わります。 ただし、 flip()メソッドを2回呼び出すことは避けてください。 これは、2回目の呼び出しで limit が0に設定され、データ要素を読み取ることができないためです。
  • 基になるデータを複数回読み取りたい場合は、 rewind()メソッドが便利です。
  • compact()メソッドは、バッファーの部分的な再利用に適しています。 たとえば、基になるデータのすべてではなく一部を読み取り、次にデータをバッファに書き込みたいとします。 compact()メソッドは、未読データをバッファーの先頭にコピーし、バッファーインデックスを変更して書き込み操作の準備をします。

3.4. 残る

hasRemaining()および remaining()メソッドは、limitpositionの関係を計算します。

limitpositionより大きい場合、 hasRemaining()trueを返します。 また、 restore()メソッドは、limitpositionの差を返します。

たとえば、バッファの位置が2で制限が8の場合、残りは6になります。

ByteBuffer buffer = ByteBuffer.allocate(10); // mark = -1, position = 0, limit = 10
buffer.position(2);                          // mark = -1, position = 2, limit = 10
buffer.limit(8);                             // mark = -1, position = 2, limit = 8
boolean flag = buffer.hasRemaining();        // true
int remaining = buffer.remaining();          // 6

4. データの転送

オニオンモデルの第2層は、データの転送に関係しています。 具体的には、 ByteBufferクラスは、他のデータ型 byte char short との間でデータを転送するメソッドを提供します。 int long float 、および double ):

4.1. バイトデータを転送する

byte データを転送するために、ByteBufferクラスは単一および一括操作を提供します。

1回の操作で、バッファーの基になるデータとの間で1バイトの読み取りまたは書き込みを行うことができます。これらの操作には、次のものが含まれます。

public abstract byte get();
public abstract ByteBuffer put(byte b);
public abstract byte get(int index);
public abstract ByteBuffer put(int index, byte b);

上記のメソッドのget() / put()メソッドの2つのバージョンに気付く場合があります。1つはパラメーターを持たず、もう1つはindexを受け入れます。 それで、違いは何ですか?

インデックスのないものは相対演算であり、現在の位置にあるデータ要素を操作し、後で位置を1つインクリメントします。 ただし、 index を持つものは全体の操作であり、 index のデータ要素を操作し、positionを変更しません。

対照的に、バルク操作では、バッファーの基になるデータとの間で複数のバイトを読み書きできます。これらの操作には次のものが含まれます。

public ByteBuffer get(byte[] dst);
public ByteBuffer get(byte[] dst, int offset, int length);
public ByteBuffer put(byte[] src);
public ByteBuffer put(byte[] src, int offset, int length);

上記のメソッドはすべて相対操作に属します。 つまり、現在の位置との間で読み取りまたは書き込みを行い、位置の値をそれぞれ変更します。

ByteBufferパラメーターを受け入れる別のput()メソッドもあります。

public ByteBuffer put(ByteBuffer src);

4.2. intデータを転送する

byte データの読み取りまたは書き込みに加えて、 ByteBuffer クラスは、booleanタイプを除く他のプリミティブタイプもサポートします。 例としてintタイプを取り上げましょう。 関連する方法は次のとおりです。

public abstract int getInt();
public abstract ByteBuffer putInt(int value);
public abstract int getInt(int index);
public abstract ByteBuffer putInt(int index, int value);

同様に、 getInt()および putInt()メソッドと index パラメーターは絶対操作であり、それ以外の場合は相対操作です。

5. さまざまなビュー

オニオンモデルの第3層は、同じ基になるデータを異なる視点で読み取るです。

上の図の各メソッドは、元のバッファーと同じ基になるデータを共有する新しいビューを生成します。 新しい見方を理解するには、次の2つの問題について心配する必要があります。

  • 新しいビューは基になるデータをどのように解析しますか?
  • 新しいビューはそのインデックスをどのように記録しますか?

5.1. ByteBuffer表示

ByteBufferインスタンスを別のByteBuffer ビューとして読み取るには、 duplicate() slave()、およびの3つのメソッドがあります。 ] asReadOnlyBuffer()

それらの違いの図を見てみましょう:

ByteBuffer buffer = ByteBuffer.allocate(10); // mark = -1, position = 0, limit = 10, capacity = 10
buffer.position(2);                          // mark = -1, position = 2, limit = 10, capacity = 10
buffer.mark();                               // mark = 2,  position = 2, limit = 10, capacity = 10
buffer.position(5);                          // mark = 2,  position = 5, limit = 10, capacity = 10
buffer.limit(8);                             // mark = 2,  position = 5, limit = 8,  capacity = 10

duplicate()メソッドは、元のインスタンスと同じように、新しいByteBufferインスタンスを作成します。 ただし、2つのバッファにはそれぞれ、独立した limit position 、およびmarkがあります。

ByteBuffer view = buffer.duplicate();        // mark = 2,  position = 5, limit = 8,  capacity = 10

slip()メソッドは、基になるデータの共有サブビューを作成します。 ビューの位置は0になり、その制限容量は元のバッファーの残りになります。

ByteBuffer view = buffer.slice();            // mark = -1, position = 0, limit = 3,  capacity = 3

duplicate()メソッドと比較すると、 asReadOnlyBuffer()メソッドは同様に機能しますが、読み取り専用バッファーを生成します。 つまり、この読み取り専用ビューを使用して、基になるデータを変更することはできません。

ByteBuffer view = buffer.asReadOnlyBuffer(); // mark = 2,  position = 5, limit = 8,  capacity = 10

5.2. その他のビュー

ByteBuffer は、他のビューも提供します: asCharBuffer() asShortBuffer() asIntBuffer()、 asLongBuffer()[ X144X]、 asFloatBuffer()、および asDoubleBuffer()。 これらのメソッドはslice()メソッドに似ています。 これらは、基になるデータの現在の位置および制限に対応するスライスされたビューを提供します。 それらの主な違いは、基になるデータを他のプリミティブ型の値に解釈することです。

私たちが気にかけるべき質問は次のとおりです。

  • 基礎となるデータを解釈する方法
  • 解釈を開始する場所
  • 新しく生成されたビューに表示される要素の数

新しいビューは、複数のバイトをターゲットプリミティブ型に構成し、元のバッファの現在の位置から解釈を開始します。 新しいビューの容量は、元のバッファー内の残りの要素の数を、ビューのプリミティブ型を構成するバイト数で割ったものに等しくなります。 最後の残りのバイトはビューに表示されません。

ここで、例として asIntBuffer()を取り上げましょう。

byte[] bytes = new byte[]{
  (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE, // CAFEBABE ---> cafebabe
  (byte) 0xF0, (byte) 0x07, (byte) 0xBA, (byte) 0x11, // F007BA11 ---> football
  (byte) 0x0F, (byte) 0xF1, (byte) 0xCE               // 0FF1CE   ---> office
};
ByteBuffer buffer = ByteBuffer.wrap(bytes);
IntBuffer intBuffer = buffer.asIntBuffer();
int capacity = intBuffer.capacity();                         // 2

上記のコードスニペットでは、 buffer には11個のデータ要素があり、intタイプは4バイトを使用します。 したがって、 intBuffer には2つのデータ要素(11/4 = 2)があり、余分な3バイト(11%4 = 3)は省略されます。

6. ダイレクトバッファ

ダイレクトバッファとは何ですか? 直接バッファとは、OS機能が直接アクセスできるメモリ領域に割り当てられたバッファの基礎となるデータを指します。 非直接バッファーとは、基礎となるデータがJavaヒープ領域に割り当てられたbyte配列であるバッファーを指します。

では、どうすれば直接バッファを作成できますか? 直接ByteBufferは、必要な容量で alllocateDirect()メソッドを呼び出すことによって作成されます。

ByteBuffer buffer = ByteBuffer.allocateDirect(10);

なぜ直接バッファが必要なのですか?答えは簡単です。非直接バッファでは常に不要なコピー操作が発生します。 非直接バッファのデータをI/Oデバイスに送信する場合、ネイティブコードは、基になる byte 配列を「ロック」し、Javaヒープの外にコピーしてから、OS関数を呼び出してフラッシュする必要があります。データ。 ただし、ネイティブコードは、基になるデータに直接アクセスし、OS関数を呼び出して、直接バッファーを使用することにより、追加のオーバーヘッドなしでデータをフラッシュできます。

上記に照らして、ダイレクトバッファは完璧ですか? いいえ。 主な問題は、直接バッファの割り当てと割り当て解除にコストがかかることです。 では、実際には、直接バッファは常に非直接バッファよりも高速に実行されますか? 必ずしも。 これは、多くの要因が関係しているためです。 また、パフォーマンスのトレードオフは、JVM、オペレーティングシステム、およびコード設計によって大きく異なる可能性があります。

最後に、従うべき実用的なソフトウェアの格言があります。最初にそれを機能させ、次にそれを高速にします。 つまり、最初にコードの正確さに集中しましょう。 コードが十分に高速に実行されない場合は、対応する最適化を実行しましょう。

7. その他

ByteBuffer クラスは、いくつかの補助メソッドも提供します。

7.1. Is関連のメソッド

isDirect()メソッドは、バッファーが直接バッファーであるか非直接バッファーであるかを示します。 ラップされたバッファー( wrap()メソッドで作成されたバッファー)は常に非直接であることに注意してください。

すべてのバッファは読み取り可能ですが、すべてが書き込み可能というわけではありません。 isReadOnly()メソッドは、基になるデータに書き込めるかどうかを示します。

これらの2つのメソッドを比較するために、 isDirect()メソッドは、Javaヒープまたはメモリ領域のどこに基になるデータが存在するかを考慮します。 ただし、 isReadOnly()メソッドは、基になるデータ要素を変更できるかどうかを考慮します

元のバッファが直接または読み取り専用の場合、新しく生成されたビューはそれらの属性を継承します。

7.2. 配列関連のメソッド

ByteBuffer インスタンスが直接または読み取り専用の場合、基になるバイト配列を取得できません。 ただし、バッファが非直接で読み取り専用ではない場合、それは必ずしもその基になるデータにアクセスできることを意味するわけではありません。

正確には、 hasArray()メソッドは、バッファーにアクセス可能なバッキング配列があるかどうかを教えてくれます hasArray()メソッドが true を返す場合、 array()および arrayOffset()メソッドを使用してより関連性を高めることができます情報。

7.3. バイトオーダー

デフォルトでは、ByteBufferクラスのバイトオーダーは常にByteOrder.BIG_ENDIANです。 また、 order()メソッドと order(ByteOrder)メソッドを使用して、それぞれ現在のバイトオーダーを取得および設定できます。

バイト順序は、基になるデータの解釈方法に影響します。 たとえば、bufferインスタンスがあるとします。

byte[] bytes = new byte[]{(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE};
ByteBuffer buffer = ByteBuffer.wrap(bytes);

ByteOrder.BIG_ENDIAN を使用すると、 val は-889275714(0xCAFEBABE)になります。

buffer.order(ByteOrder.BIG_ENDIAN);
int val = buffer.getInt();

ただし、 ByteOrder.LITTLE_ENDIAN を使用すると、 val は-1095041334(0xBEBAFECA)になります。

buffer.order(ByteOrder.LITTLE_ENDIAN);
int val = buffer.getInt();

7.4. 比較する

ByteBuffer クラスは、2つのバッファーインスタンスを比較するための equals()および compareTo()メソッドを提供します。 これらの方法は両方とも、 [position、limit)の範囲にある残りのデータ要素に基づいて比較を実行します。

たとえば、基になるデータとインデックスが異なる2つのバッファインスタンスを等しくすることができます。

byte[] bytes1 = "World".getBytes(StandardCharsets.UTF_8);
byte[] bytes2 = "HelloWorld".getBytes(StandardCharsets.UTF_8);

ByteBuffer buffer1 = ByteBuffer.wrap(bytes1);
ByteBuffer buffer2 = ByteBuffer.wrap(bytes2);
buffer2.position(5);

boolean equal = buffer1.equals(buffer2); // true
int result = buffer1.compareTo(buffer2); // 0

8. 結論

この記事では、ByteBufferクラスをタマネギモデルとして扱うことを試みました。 最初に、それをbyte配列のコンテナーに単純化して追加のインデックスを付けました。 次に、 ByteBuffer クラスを使用して、他のデータ型との間でデータを転送する方法について説明しました。

次に、同じ基になるデータを異なるビューで調べました。 最後に、ダイレクトバッファといくつかのさまざまな方法について説明しました。

いつものように、このチュートリアルのソースコードはGitHubにあります。