ストリームコピーって何をするユーティリティなのか
「ストリームコピー」は、InputStream から OutputStream へバイト列をそのまま流す処理です。 ざっくり言うと「どこかから読んだデータを、どこかへそのまま書き出す」ための共通パターンで、ファイルコピー、アップロードファイルの保存、ダウンロードレスポンスの生成、プロキシ的な転送など、業務システムのあちこちで使われます。
毎回この処理を手書きすると、バッファサイズのミス、bytesRead の扱いミス、ストリームの閉じ忘れなど、典型的なバグを量産しがちです。 だからこそ、「ストリームコピー」をユーティリティとして一度きれいにまとめておく価値が大きいのです。
いちばん基本の形:バッファを使ったコピーループ
InputStream と OutputStream をバイト配列でつなぐ
古典的で、どの Java バージョンでも使える基本形は「バイト配列バッファを使ったループ」です。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class StreamCopyUtils {
public static long copy(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[8192]; // 8KB バッファ
long total = 0;
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len); // 読めた分だけ書く
total += len;
}
return total;
}
}
Javaここで深掘りしたい重要ポイントは三つあります。
一つ目は「バッファサイズ」です。 byte[8192] や byte[4096] がよく使われますが、これは「1バイトずつ読むより圧倒的に速い」「OSのページサイズと相性が良い」といった理由から来ています。
二つ目は「len を必ず使う」ことです。 read(buffer) は、バッファサイズぴったり読めるとは限らず、「今回実際に読めたバイト数」を返します。 out.write(buffer) としてしまうと、前回の残りやゴミまで書いてしまい、データが壊れます。 必ず out.write(buffer, 0, len) のように「有効な範囲だけ」書く必要があります。
三つ目は「ストリームをどこで閉じるか」です。 このユーティリティは「コピーだけ」を責務にして、close() は呼びません。 呼び出し側で try-with-resources を使って開閉を管理する方が、責務の分離としてきれいです。
呼び出し側の定番パターン:try-with-resources と組み合わせる
ファイルコピーの例でイメージを固める
先ほどのユーティリティを使って、ファイルコピーをしてみます。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class FileCopyExample {
public static void main(String[] args) throws Exception {
try (InputStream in = new FileInputStream("data/source.bin");
OutputStream out = new FileOutputStream("data/dest.bin")) {
long copied = StreamCopyUtils.copy(in, out);
System.out.println("コピーしたバイト数: " + copied);
}
}
}
Javaここで重要なのは、try-with-resources によって in と out が必ず閉じられることです。 ストリームコピー自体は「流すだけ」に集中し、リソース管理は呼び出し側で行う、という役割分担ができています。
このパターンを覚えておくと、ファイルだけでなく、ソケットや HTTP レスポンスでも同じ形で使い回せます。
Java 9 以降なら InputStream.transferTo が使える
標準ライブラリが用意してくれた「ストリームコピー専用メソッド」
Java 9 から、InputStream に transferTo(OutputStream out) というメソッドが追加されました。
これはまさに「ストリームコピーをやりたいときのためのメソッド」で、内部でバッファループを回してくれます。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class StreamCopyUtilsJ9 {
public static long copy(InputStream in, OutputStream out) throws IOException {
return in.transferTo(out);
}
}
Java使い方は先ほどと同じで、呼び出し側で try-with-resources を使います。
try (InputStream in = new FileInputStream("data/source.bin");
OutputStream out = new FileOutputStream("data/dest.bin")) {
long copied = StreamCopyUtilsJ9.copy(in, out);
System.out.println("コピーしたバイト数: " + copied);
}
Javaここで深掘りしたいのは、「transferTo はストリームを閉じない」という仕様です。 あくまで「読み書きだけ」をしてくれるので、やはりリソース管理は呼び出し側で行う必要があります。 この点を理解しておけば、「コピーしたあとに別の処理をしたい」「途中でフラッシュしたい」といった拡張もやりやすくなります。
Files.copy を使った「ファイル+ストリーム」コピー
Path と組み合わせるときのユーティリティ
ファイルとストリームをつなぐ場合は、java.nio.file.Files.copy も便利です。
例えば、「アップロードされた InputStream をファイルに保存する」ユーティリティはこう書けます。
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class UploadSaver {
public static long save(InputStream in, Path target) throws IOException {
return Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
}
}
Java使い方のイメージです。
// 例えば Servlet や Web フレームワークから渡される InputStream
InputStream uploadStream = ...;
Path target = Path.of("uploads/data.bin");
long size = UploadSaver.save(uploadStream, target);
System.out.println("保存サイズ: " + size + " バイト");
Javaここで重要なのは、「上書きするかどうか」をオプションで明示していることです。 StandardCopyOption.REPLACE_EXISTING を付けないと、既存ファイルがある場合に例外になります。 業務的に「同名ファイルがあったらどうするか」は、セキュリティ・運用の観点からも重要な設計ポイントなので、ユーティリティ側で方針を決めておくと安心です。
ストリームコピーで絶対に押さえておきたい注意点
部分読み込みと部分書き込みを正しく扱う
ストリームコピーのバグでいちばん多いのが、「読めた分だけ書いていない」問題です。 read(buffer) の戻り値を無視して write(buffer) してしまうと、 最後のチャンクで余計なデータを書いたり、前回の残りが混ざったりして、バイナリが壊れます。
必ず「len バイトだけが今回有効」という意識を持ち、write(buffer, 0, len) を徹底してください。
ストリームの閉じ忘れをユーティリティで防ぐか、呼び出し側で防ぐか
もう一つの典型的な問題が「ストリームを閉じ忘れる」ことです。 ファイルハンドルやソケットが開きっぱなしになると、長期運用で枯渇して障害になります。
設計としては二つの方針があります。
ユーティリティは「コピーだけ」に集中し、close() は呼ばない。 呼び出し側で try-with-resources を使って開閉を管理する。
あるいは、
「このユーティリティは InputStream/OutputStream のライフサイクルも管理する」と決めて、 内部で try-with-resources を使い、渡されたストリームを閉じてしまう。
初心者向けには前者の方が分かりやすく、柔軟性も高いです。 「誰が閉じるか」を明確に決めておくことが、セキュリティ・運用の観点でも大事なポイントになります。
まとめ:ストリームコピーユーティリティで身につけてほしい感覚
ストリームコピーユーティリティは、「InputStream から OutputStream へ、安全に・効率よくバイトを流す」ための共通パターンです。 その中には、次のような感覚が詰まっています。
バッファを使って少しずつ読み書きすることで、性能とメモリのバランスを取る。 read の戻り値(実際に読めたバイト数)を必ず使い、データ破壊を防ぐ。 ストリームの開閉は try-with-resources などで確実に行い、リソースリークを防ぐ。 Java 9 以降なら transferTo、ファイルとの連携なら Files.copy など、標準の便利メソッドも積極的に活用する。
もし今、あなたのコードのあちこちに「似たようなコピー処理」が散らばっているなら、 それを一度「ストリームコピーユーティリティ」にまとめてみてください。
それだけで、I/O・ネットワークまわりのコードがすっきりし、 バグの温床になりがちな低レベル処理を、安心して再利用できる形に育てていけます。
