1. なぜ DataInputStream と DataOutputStream が必要か?
ファイルを扱うとき、単なるテキストではなく構造化されたデータ(数値、真偽値、プリミティブ配列など)を保存したいことがあります。例えば、簡単なゲームを書いていてユーザーの進行状況を保存したいとします。スコア(int)、現在のレベル(int)、プレイ時間(double)、勝者かどうか(boolean)などです。もちろん、テキストとして次のように保存することもできます。
12345
5
67.5
true
しかしこれは扱いづらく安全でもありません。文字列をパースする必要があり、フォーマットが崩れるかもしれず、数値はより多くの容量を取ります。
発想は、テキストに変換せずにデータを「生の」(バイナリ)形式でファイルへ記録することです。そのために Java には次の便利な 2 つのクラスがあります。
- DataOutputStream はプリミティブ型をストリームに書き込めます。
- DataInputStream はプリミティブ型をストリームから読み取れます。
これらは通常のバイトストリーム(OutputStream/InputStream)の上に重ねて使います。つまり単にバイトを書くだけでなく、そのバイト列から int、double、boolean、さらには String までを組み立てる方法を知っている「アダプタ」のようなものです。
どのように動作するのか?
通常の FileOutputStream は、手作業でバイトを 1 つずつベルトコンベアに流すようなものです。整数や文字列を書き込みたいなら、各要素が何バイトかを自分で管理しなければなりません。
DataOutputStream を使うと楽になります。まるでコンベアで働くロボットのように、「数値を書いて」「文字列を書いて」と指示すれば、必要なバイト数に自動でパックしてディスクへ送ります。コンベアの反対側には同じロボットである DataInputStream がいて、そのバイト列から元のオブジェクトを再構成できます。
なぜ便利なのか? int、double、boolean のバイト数を気にする必要がありません。データはコンパクトに保存でき、高速に読み書きでき、パースエラーやフォーマット起因の問題を避けられます。
2. 例: プリミティブの書き込みと読み取り
仮にゲームの結果を保存したいとします。プレイヤー名(String)、スコア(int)、記録タイム(double)、勝者かどうか(boolean)です。
データをファイルに書き込む
import java.io.*;
public class SaveGameData {
public static void main(String[] args) {
String fileName = "savegame.bin";
String playerName = "Alice";
int score = 12345;
double recordTime = 67.5;
boolean isWinner = true;
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream(fileName))) {
dos.writeUTF(playerName); // 文字列を書き込む(UTF-8)
dos.writeInt(score); // int を書き込む(4 バイト)
dos.writeDouble(recordTime); // double を書き込む(8 バイト)
dos.writeBoolean(isWinner); // boolean を書き込む(1 バイト)
System.out.println("データをファイルに正常に書き込みました!");
} catch (IOException e) {
System.out.println("書き込みエラー: " + e.getMessage());
}
}
}
- writeUTF(String) — 先頭に長さを付けて UTF-8 形式で文字列を書き込みます。
- writeInt(int) — 4 バイトを書き込みます。
- writeDouble(double) — 8 バイトを書き込みます。
- writeBoolean(boolean) — 1 バイト(1 または 0)を書き込みます。
- すべてのメソッドは必要な形式に自動的にデータを「パック」します。
ファイルからデータを読み取る
import java.io.*;
public class LoadGameData {
public static void main(String[] args) {
String fileName = "savegame.bin";
try (DataInputStream dis = new DataInputStream(
new FileInputStream(fileName))) {
String playerName = dis.readUTF(); // 文字列を読む
int score = dis.readInt(); // int を読む
double recordTime = dis.readDouble(); // double を読む
boolean isWinner = dis.readBoolean(); // boolean を読む
System.out.println("プレイヤー名: " + playerName);
System.out.println("スコア: " + score);
System.out.println("記録タイム: " + recordTime);
System.out.println("勝者: " + isWinner);
} catch (IOException e) {
System.out.println("読み取りエラー: " + e.getMessage());
}
}
}
重要なポイント:
読み取り順序は、書き込み順序と一致させる必要があります! 最初に文字列、次に int、その後に double を書いたなら、読み取りも同じ順序で行ってください。そうしないとエラーやデータの破損につながります。
3. どの型がサポートされている?
DataOutputStream と DataInputStream は、Java の主要なプリミティブ型をすべてサポートしています。
| 書き込みメソッド | 読み取りメソッド | データ型 | サイズ(バイト) |
|---|---|---|---|
|
|
boolean | 1 |
|
|
byte | 1 |
|
|
short | 2 |
|
|
char | 2 |
|
|
int | 4 |
|
|
long | 8 |
|
|
float | 4 |
|
|
double | 8 |
|
|
String (UTF) | 可変 |
注意:
- 文字列には一般的に writeUTF/readUTF を使います(先頭に長さを書き、その後に UTF-8 のバイト列を書きます)。
- 配列を書き込みたい場合は、最初に長さを書き、その後に要素を 1 つずつ書き込みます。
4. 発展例: プリミティブ配列を保存する
配列の書き込み
int[] scores = {100, 200, 300, 400, 500};
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream("scores.bin"))) {
dos.writeInt(scores.length); // まず配列の長さを書き込む
for (int score : scores) {
dos.writeInt(score); // 次に各要素
}
}
配列の読み取り
try (DataInputStream dis = new DataInputStream(
new FileInputStream("scores.bin"))) {
int length = dis.readInt(); // 長さを読み取る
int[] scores = new int[length];
for (int i = 0; i < length; i++) {
scores[i] = dis.readInt(); // 要素を読み取る
}
// 配列を出力する
for (int score : scores) {
System.out.println(score);
}
}
なぜ最初に長さなのか?
読み取り時に、いくつの数値が書かれているか分からないためです。先頭に長さを書いておけば、ファイル形式が自己記述的になります。
5. 重要な注意点と特徴
いつ DataInputStream/DataOutputStream を使うべきか?
- プリミティブから成る構造化データを保存・読み込みしたいとき。
- Java プログラム間(あるいはフォーマットが分かっていれば異なる言語間)でバイナリデータをやり取りするとき。
- コンパクトさと速度が重要なとき(例: ログ、計算結果、大きな数値配列)。
使うべきでない場合:
- 人が読める形式(CSV、JSON、XML)が必要な場合はテキストフォーマットを使用してください。
- 入れ子を含む複雑なオブジェクトには、ObjectOutputStream/ObjectInputStream によるシリアライズを使うのがよいでしょう(別テーマ)。
バッファリング
DataOutputStream と DataInputStream 自体はバッファリングしません。大きなファイルで性能を上げたい場合は、BufferedOutputStream/BufferedInputStream で包んでください。
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
// ...
}
文字列のエンコーディング
writeUTF/readUTF は特別なフォーマットを使います。先に文字列の長さ(バイト数)を書き、その後に UTF-8 の内容を書きます。単なるバイト配列の書き込みと混同しないでください。
例外
読み書きの操作は、ファイルが利用できない・破損している・途中で終わっている場合などに IOException をスローすることがあります。書かれた以上のデータを読もうとすると、しばしば EOFException が発生します。常に try-with-resources 構文を使うか、try-catch で例外を処理してください。
読み取りと書き込みの順序
最も多いミスは、読み取りと書き込みの順序が一致しないことです。例えば int、double、boolean の順に書いたのに、double、int、boolean の順に読めば、誤ったデータか例外になります。
6. 典型的なミス
エラー No.1: 読み書きの順序を崩す。 メソッドの順番を変えると、データが誤って解釈されるか、例外が投げられます。例えば、先に文字列、その後に数値を書いたのに、読み取りで先に数値を読もうとするとフォーマットエラーになります。
エラー No.2: 配列の長さを書き忘れる。 プリミティブ配列を書くときに長さを書かなければ、読み取り時にいくつ読むべきか分かりません。これはエラー、あるいは末尾に「余分な」データが残る原因になります。
エラー No.3: ファイル末尾を越えて読もうとする。 書かれている以上のデータを読めば、EOFException(end of file)が発生します。
エラー No.4: テキストファイルに DataInputStream/DataOutputStream を使う。 これらのクラスは、例えばメモ帳で作成された通常のテキストファイルを読むためのものではありません。そのようなファイルを readInt() で読もうとすると、無意味なデータかエラーになります。
エラー No.5: ストリームを閉じない。 try-with-resources を使わない、または手動でストリームを閉じない場合、ファイルが他のプログラムから利用できなくなったり、完全に保存されなかったりすることがあります。
GO TO FULL VERSION