Java Tips | I/O・ネットワーク:ストリームコピー

Java Java
スポンサーリンク

ストリームコピーって何をするユーティリティなのか

「ストリームコピー」は、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 によって inout が必ず閉じられることです。 ストリームコピー自体は「流すだけ」に集中し、リソース管理は呼び出し側で行う、という役割分担ができています。

このパターンを覚えておくと、ファイルだけでなく、ソケットや HTTP レスポンスでも同じ形で使い回せます。

Java 9 以降なら InputStream.transferTo が使える

標準ライブラリが用意してくれた「ストリームコピー専用メソッド」

Java 9 から、InputStreamtransferTo(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・ネットワークまわりのコードがすっきりし、 バグの温床になりがちな低レベル処理を、安心して再利用できる形に育てていけます。

タイトルとURLをコピーしました