【Java】ByteBufferの使い方

2020年5月17日

ゲームでバイナリ形式でデータ保存する場面で使用する機会がありましたので、基本操作のみざっと記事にしておきます。

主にFileChannelやSocketChannelなどで利用することになると思います。もちろん単体での利用も可能で簡単にバイト配列化できるので、覚えておくと意外と役に立ちます。





 

ByteBufferとは

ByteBufferとはバイト配列の操作を行えるメソッドが多数定義されており、バイナリ形式でファイルの入出力をする場合や、通信プログラムの実装時などにも使えます。

データ操作で行った分マーク位置を移動してくれるため、何バイト目にこのデータを書き込み、その後ろにはこのデータ・・・というように順次書き込み、読み込みしやすいメソッドが利用できます。

下記は例です。




ByteBufferはallocateメソッドでインスタンス化します。

ByteBuffer bb = ByteBuffer.allocate(確保するデータ用量);

現在位置や上限値などを保持しており、ByteBufferを取り扱うメソッド群はこれらの位置、上限などを考慮した動作をします。

データの書き込み、put系メソッド

バッファへの書き込みはput系メソッドで行い、書き込んだ分、現在位置を移動させます。例えばint型は4バイトですが、putIntメソッドでデータを書き込むと現在位置+4バイトの位置へと移動することになります。

import java.nio.ByteBuffer;

public class Test {

	public static void main(String[] args) {
		//1024バイトのバッファを持つByteBufferをインスタンス化
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		//書き込み用データ
		int a=10;
		
		//4バイト書き込み
		bb.putInt(a);
	}
}

データの読み込み、get系メソッド

バッファの読み込みはget系メソッドで行い、読み込んだ分、現在位置を移動させます。例えばlong型は8バイトですが、getLongメソッドでデータを読み込むと、現在位置+8バイトの位置へと移動することになります。

import java.nio.ByteBuffer;

public class Test {

	public static void main(String[] args) {
		//1024バイトのバッファを持つByteBufferをインスタンス化
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		//書き込み用データ
		long a=102178634;
		
		//8バイト書き込み
		bb.putLong(a);
		
		//0バイト目へ移動
		bb.position(0);
		
		//0バイト目から8バイト目までを表示
		System.out.println(bb.getLong());
	}
}
実行結果

102178634

データの読み込み準備をするflip

現在のデータを読み込むための準備を行います。

ByteBufferを取り扱うメソッドは現在位置や上限を考慮した動作を行うため、データを書き込んで即座にJavaメソッドに渡しても意図しない動作となります。書き込んだ位置から上限値を処理しようとしてしまうからですね。これを回避するには上限値を現在位置に設定し、現在位置は0に戻す処理が必要です。そんなときに使用できるメソッドがflipメソッドです。

flipメソッドは上限値を現在位置に設定し、現在位置は0に戻す処理を実行できます。



意外と使用する場面は多いため覚えておいたほうがいいでしょう。

図では1バイト目からで説明しましたが、プログラムでは0バイト目から始まります。

import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;

public class Test {

	public static void main(String[] args) {
		//1024バイトのバッファを持つByteBufferをインスタンス化
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		//書き込み
		bb.put("Hello".getBytes(StandardCharsets.UTF_8));
		
		//0バイト目へ移動し、上限値を現在に設定
		bb.flip();
		
		//結果を表示
		System.out.println(StandardCharsets.UTF_8.decode(bb));
	}
}
実行結果

Hello

試しにflipなしで処理させたり、position(0)で処理させたりすると違いがわかりやすいかもしれませんね。

データを初期化するclear

データの入出力を始めから行いたい場合はclearメソッドを呼び出します。現在位置を0に設定し、上限値を容量の値に設定します。

使いどころは使用済みのByteBufferを使いまわしたいときでしょうか。

都度確保するよりも使い回しをしたほうが、メモリなどいろいろ節約できそうですね。

まあflipほど重要なメソッドではないでしょう。

import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;

public class Test {

	public static void main(String[] args) {
		//1024バイトのバッファを持つByteBufferをインスタンス化
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		//書き込み
		bb.put("TEST".getBytes(StandardCharsets.UTF_8));
		
		//0バイト目へ移動し、上限値を現在に設定
		bb.flip();
		
		//結果を表示
		System.out.println(StandardCharsets.UTF_8.decode(bb));
		
		//使いまわすために初期化
		bb.clear();
		
		//書き込み
		bb.put("ABC".getBytes(StandardCharsets.UTF_8));
		
		//0バイト目へ移動し、上限値を現在に設定
		bb.flip();
		
		//結果を表示
		System.out.println(StandardCharsets.UTF_8.decode(bb));
	}
}
実行結果

TEST
ABC

再度同じ処理を行うrewind

データの入出力を再度行いたい場合はrewindメソッドを利用します。現在位置を0に設定します。

例えば読み込み準備を行った後、再度0バイト目から読みこむ処理を行いたい場面などで使えます。

基本的にはそれほど使用頻度は高くないメソッドでしょう。

import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;

public class Test {

	public static void main(String[] args) {
		//1024バイトのバッファを持つByteBufferをインスタンス化
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		//書き込み
		bb.put("TEST".getBytes(StandardCharsets.UTF_8));
		
		//読み込み準備
		bb.flip();
		
		//結果を表示
		System.out.println(StandardCharsets.UTF_8.decode(bb));
		
		//現在の状態から再度書き込み準備
		bb.rewind();
		
		//再度結果を表示
		System.out.println(StandardCharsets.UTF_8.decode(bb));
	}
}
実行結果

TEST
TEST

rewindを呼び出さずに処理するとTESTが二回表示されなくなるはずです。

現在位置を取得、設定するposition

現在位置を指定したり、取得します。通常は利用することはないかもしれませんが、読み飛ばしなどで利用する場面もあるかもしれません。

import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;

public class Test {

	public static void main(String[] args) {
		//1024バイトのバッファを持つByteBufferをインスタンス化
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		//書き込み
		bb.put("TEST".getBytes(StandardCharsets.UTF_8));
		
		//読み込み準備
		bb.flip();
		
		//2バイト目へ移動(最初の二文字を飛ばす)
		bb.position(2);
		
		//結果を表示
		System.out.println(StandardCharsets.UTF_8.decode(bb));
	}
}
実行結果

ST

 

上限値を取得、設定するlimit

上限値を設定したり、取得できます。通常は手動で利用することはそんなにないかと思われます。

import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;

public class Test {

	public static void main(String[] args) {
		//1024バイトのバッファを持つByteBufferをインスタンス化
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		//書き込み
		bb.put("TEST".getBytes(StandardCharsets.UTF_8));
		
		//0バイト目へ
		bb.rewind();
		
		//2バイトまでを上限に
		bb.limit(2);
		
		//結果を表示
		System.out.println(StandardCharsets.UTF_8.decode(bb));
	}
}
実行結果

TE

 

残データの容量を取得するremaining

残りのデータをバイト配列で取得したい場合に使いたい場面もあると思います。

import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;

public class Test {

	public static void main(String[] args) {
		//1024バイトのバッファを持つByteBufferをインスタンス化
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		//書き込み
		bb.put("TESTERREMAINING".getBytes(StandardCharsets.UTF_8));
		
		//読み込み準備
		bb.flip();
		
		//4バイト読み込み
		bb.getInt();
		
		//残りのバイト数で結果バッファを作成
		byte[] b = new byte[bb.remaining()];
		
		//バッファ分読み込み
		bb.get(b);
		
		//結果を表示
		System.out.println(new String(b,StandardCharsets.UTF_8));
	}
}
実行結果

ERREMAINING

事前にgetIntで4バイト読み込んでいるので最初の4文字以降の文字が表示されます。

サンプルプログラム

それでは実際に利用する際のサンプルプログラムを紹介します。

サンプルでは、最初の4バイトをint型の数値、次の2バイトをshort型の数値、残りのデータを文字列として書き込み、読み込みを行うことを想定しています。通信やバイナリファイルを入出力する場合に使えそうですよね。

それではサンプルをご覧ください。

import java.nio.charset.StandardCharsets;
import java.nio.ByteBuffer;
import java.io.UnsupportedEncodingException;

public class Test {

	public static void main(String[] args) throws UnsupportedEncodingException{
		//1024バイトのバッファを持つByteBufferをインスタンス化
		ByteBuffer bb = ByteBuffer.allocate(1024);
		
		//書き込み用データ
		int a=10;
		short b=20;
		String c="Hello World";
		
		//4バイトのint型書き込み
		bb.putInt(a);
		
		//2バイトのshort型書き込み
		bb.putShort(b);
		
		//残りのデータを文字列として扱い、書き込み
		bb.put(c.getBytes(StandardCharsets.UTF_8));
		
		//現在書き込み済みデータで確定させ、読み込み準備を行う
		bb.flip();
		
		//変数aの書き込み値を表示(0バイト目から4バイト読み込み)
		System.out.println(bb.getInt());
		
		//変数bの書き込み値を表示(4バイト目から2バイト読み込み)
		System.out.println(bb.getShort());
		
		//変数cの書き込み値を表示(6バイト目から残り全部)
		System.out.println(StandardCharsets.UTF_8.decode(bb));
	}
}

10
20
Hello World

呼び出すごとに書き込み位置が自動で変更されていることがわかります。

因みにStandardCharsets.UTF_8.decode(ByteBuffer)メソッドはByteBufferの残りのデータ(現在位置から上限値まで)を文字列としてデコードする処理ができます。

Java

Posted by nompor