1. 導入
まず基本から始めましょう。バイナリファイルはテキストファイルと何が違うのでしょうか。テキストファイルとは、普通のメモ帳で開いて文字や数字、スペースなどの記号が読めるファイルのことです。例えば、my_notes.txt や poem.txt です。
バイナリファイル は、テキストではなく任意のバイト列を含むファイルです。画像(.jpg、.png)、音楽(.mp3)、アーカイブ(.zip)、実行ファイル(.exe)、動画(.mp4)、データベースファイルなどが該当します。こうしたファイルをメモ帳で開くと、ÿØÿà のようなものや意味不明な記号の羅列が見えるでしょう。これは正常です! コンピュータは「バイト」しか理解しません。コンピュータにとってテキストも画像も動画も、単なるバイト列なのです。テキストファイルではそのバイトを文字として解釈できますが、バイナリでは人間が読むための「生の」データであり、可読性は想定されていません。
バイナリファイル操作の基本クラス
Java ではバイナリファイルの操作にバイトストリームを使います:
- InputStream — バイトを読み取るための基底クラス。
- OutputStream — バイトを書き込むための基底クラス。
ファイルに対して使用する具体的な実装は次のとおりです:
- FileInputStream — ファイルからバイトを読み取ります。
- FileOutputStream — ファイルへバイトを書き込みます。
FileReader と FileWriter を聞いたことがあるかもしれませんが、これらは文字を扱うためのものでテキスト専用です。バイナリファイルには InputStream/OutputStream とそのサブクラスだけを使ってください。
2. バイナリファイルの読み取り
1 バイトずつ読む
最も簡単な方法は、ファイルを 1 バイトずつ読むことです。分かりやすい反面、とても遅いです。
try (FileInputStream in = new FileInputStream("image.jpg")) {
int b;
while ((b = in.read()) != -1) {
// b は 0 〜 255(バイト)の数値で、-1 はファイル終端を示します
// バイトを処理できます。例えば全バイトの合計を計算するなど
}
}
メソッド read() は次のバイトを int(0 から 255)として返し、ファイルの終わりに到達すると -1 を返します。通常、1 バイトずつ読むのは、ファイル構造の解析など、非常に特別な目的がある場合だけです。
ブロック(バッファ)で読む
1 バイトずつ読むのは、リンゴを1個ずつ買いに行くようなものです。袋でまとめて買ったほうがずっと効率的です! Java には read(byte[] buffer) というメソッドがあり、配列をファイルからのバイトで埋めてくれます。
try (FileInputStream in = new FileInputStream("image.jpg")) {
byte[] buffer = new byte[4096]; // 4 KB のバッファ
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
// buffer にはファイルから bytesRead バイトが入っています
// これらのバイトを処理できます。例えばどこか別の場所に保存するなど
}
}
read(buffer) は実際に読み取れたバイト数を返します(特に最後の読み取りでは、バッファサイズより少ないことがあります)。この方法はディスクへのアクセス回数が少なくなるため、はるかに高速です。
例: ファイルのコピー
任意のバイナリファイル(例えば画像)を別の場所にコピーする簡単なプログラムを書きます。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BinaryCopyExample {
public static void main(String[] args) {
String source = "cat.jpg";
String dest = "cat_copy.jpg";
try (FileInputStream in = new FileInputStream(source);
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 8 KB — 多くの用途で最適なサイズ
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("コピーが完了しました!");
} catch (IOException e) {
System.out.println("コピー時のエラー: " + e.getMessage());
}
}
}
とても簡単です。元ファイルからブロックで読み取り、そのまま新しいファイルへ書き込みます。この方法は画像、アーカイブ、動画など、あらゆるファイルで機能します。
3. バイナリファイルの書き込み
バイト配列の書き込み
バイト配列がある場合(例えばネットワークから受け取った、あるいはプログラムで生成したもの)、次のようにファイルへ書き込めます:
byte[] data = new byte[] {1, 2, 3, 4, 5}; // 配列の例
try (FileOutputStream out = new FileOutputStream("data.bin")) {
out.write(data); // 配列全体をファイルに書き込みます
}
メソッド write(byte[]) は配列内のすべてのバイトを書き込みます。配列の一部だけを書き込むこともできます: out.write(data, offset, length)。
ファイルを分割して書く(例: コピー時)
読み取り時と同様、通常はバッファを使います:
try (FileInputStream in = new FileInputStream("source.bin");
FileOutputStream out = new FileOutputStream("dest.bin")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
ここではファイルから読み取ったものを、そのまますぐに別のファイルへ書き込みます。この種のコードはアーカイバ、ダウンローダ、画像処理ツールなどでよく見られます。
4. 役に立つポイント
なぜ Reader/Writer をバイナリファイルに使ってはいけないのか?
Reader と Writer は(char)という文字を扱い、バイトではありません。これらは(例えば UTF-8 などの)エンコーディングに従ってバイトを文字に自動変換します。テキストには便利ですが、バイナリファイルに対しては致命的です!
FileWriter で画像を書こうとすると、開けない壊れたファイルになります。覚えておいてください: 非テキストのファイルには必ず InputStream/OutputStream とそのサブクラスだけを使いましょう!
バイナリファイル操作の重要な違いと注意点
- バッファサイズ: バッファが小さすぎると(ディスクアクセスが増えるため)遅くなり、大きすぎると余分なメモリを消費します。4–16 KB が一般的に最適です。
- エラー処理: 常に IOException を処理してください。ファイルが存在しない、ロックされている、ディスク容量が尽きるといったことがあり得ます。
- ストリームのクローズ: try-with-resources を使えば、エラーが起きても確実にファイルを閉じられます。
- 上書きについて: new FileOutputStream("file.bin") で開くと上書きされます。末尾に追記するには、append = true のコンストラクタを使用してください。
- アクセス権: プログラムがファイルを開けない場合、読み取り/書き込みの権限を確認しましょう。
- readAllBytes(): ファイル全体を 1 行でバイト配列に読み込めます。大きなファイルには使わないでください。メモリを使い果たしてしまいます!
5. バイナリファイル操作のよくあるミス
エラー No.1: バイナリファイルに FileReader/FileWriter を使う。 これらのクラスはバイトを文字へ、またはその逆へ変換するため、画像やアーカイブなどに対してはデータ破損を招きます。
エラー No.2: read() の戻り値を無視する。 メソッド read(byte[]) は、要求したサイズより少ないバイト数しか読めないことがあります(特に最後のブロック)。実際に処理すべきバイト数を知るため、必ず戻り値を使いましょう。
エラー No.3: ストリームを閉じ忘れる。 ファイルを閉じないとロックされたままになったり、(特に書き込み時に)データが最後まで書き出されないことがあります。try-with-resources を使いましょう。
エラー No.4: サイズを考慮せずにファイル全体をメモリへ読み込もうとする。 大きなファイルでは OutOfMemoryError を引き起こします。バッファを使い、分割して読み取りましょう。
エラー No.5: 例外を処理しない。 ファイル操作では常にエラーが起こり得ます。ファイルが見つからない、権限がない、ディスクが満杯など。IOException の処理を忘れないでください。
GO TO FULL VERSION