読み込み中...

JavaのOutputStream!9つの実践サンプルでプログラミング理解

JavaのOutputStream解説記事のサムネイル Java
この記事は約36分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Javaでファイル操作、ネットワーク送信、Zip作成、Base64変換を扱うとき、出力の入口になるのがOutputStreamです。テキストもバイナリも、最終的にはバイト列として外部へ流れるため、プログラミングではwrite()flush()close()try-with-resourcesの関係を押さえる必要があります。

その理解が曖昧なままだと、文字化け、ファイル破損、ネットワークでの送信漏れ、例外処理の不足が起きやすくなります。一方、FileOutputStreamBufferedOutputStreamを適切に組み合わせると、Javaの標準APIだけでテキスト保存、バイナリ保存、Zip圧縮、Base64エンコーディング、シリアライゼーションまで扱えますし、ここがポイントです。

公式ドキュメントによれば、OutputStreamは出力バイトを受け取り、ファイルやソケットなどの出力先へ送る抽象クラスです。Java SEのBase64.EncoderZipOutputStreamも、出力ストリームをラップして処理を追加する設計になっています。

Javaとは

Javaは、JVM上で動作するオブジェクト指向のプログラミング言語です。OSの違いをJVMが吸収するため、同じJavaコードを複数の環境へ展開しやすく、サーバーサイド、業務アプリ、Android関連、組み込み領域まで広く使われます。

この言語の特徴は、標準ライブラリの範囲が広い点にもあるのが基本です。ファイル操作にはjava.iojava.nio.file、ネットワークにはjava.net、Zipにはjava.util.zip、Base64にはjava.util.Base64が用意され、外部ライブラリを増やさずに基本処理を組み立てられます。

そのため、Javaのプログラミング学習では、文法だけでなく標準APIの組み合わせ方が理解の軸になります。特にInputStreamOutputStreamは、テキスト、バイナリ、シリアライゼーション、ネットワーク処理を横断するため、早い段階で動きを整理しておくと応用しやすくなるのが目安です。

動作確認環境
  • Java: JDK 21
  • 主要API: java.io / java.net / java.util.zip / java.util.Base64
  • 文字コード例: StandardCharsets.UTF_8
📖 この記事で学べること
  • JavaのOutputStreamがバイト出力を扱う仕組み
  • テキストとバイナリのファイル操作で使う基本コード
  • ネットワーク送信、Zip、Base64、シリアライゼーションへの応用
  • flush()close()、例外処理でつまずきやすい点
  • 独自OutputStreamを作るときの設計の考え方

OutputStreamとは

OutputStreamは、Javaでバイト列を出力先へ送るための抽象クラスです。出力先はファイル、メモリ、ネットワークソケット、圧縮ストリームなどに分かれますが、呼び出し側は主にwrite()でデータを書き込みます。

これにより、出力先が変わってもプログラミング上の考え方は大きく変わりません。ファイル操作ならFileOutputStream、一時的なバイナリ保持ならByteArrayOutputStream、処理を重ねたいならFilterOutputStreamBufferedOutputStreamを選びます。

ただし、OutputStream自体はバイトを扱うAPIであり、文字列の意味までは管理しません。そのため、テキストを扱う場合はgetBytes()OutputStreamWriterでエンコーディングを明示し、UTF-8などの文字コードをそろえる設計が必要になるのがポイントです。

用途主なクラス/API扱うデータ注意点
テキスト保存FileOutputStreamUTF-8などのテキストgetBytes()のエンコーディングをそろえます
バイナリ保存FileOutputStream画像や独自形式のバイナリ文字列として開くと内容が読みにくくなります
小刻みな書き込みBufferedOutputStream複数回のバイト出力flush()close()で残りを流します
メモリ出力ByteArrayOutputStreamバイト配列toByteArray()で取得します
ネットワーク送信Socket#getOutputStream()送信メッセージ相手サーバーの待機状態が必要です
Zip作成ZipOutputStream圧縮エントリputNextEntry()closeEntry()を対応させます
Base64変換Base64.Encoder#wrap()エンコード済みテキスト閉じるタイミングで終端処理が行われます
シリアライゼーションObjectOutputStreamオブジェクトグラフSerializableの実装が必要です
文字変換OutputStreamWriter文字列StandardCharsets.UTF_8を使うと明確です
例外処理IOExceptionI/O失敗原因別にメッセージを分けます
ファイル未発見FileNotFoundExceptionパスや権限エラー親ディレクトリの存在も確認します
自動クローズtry-with-resourcesリソース管理Java 7以降で利用できます
明示クローズclose()OSリソース二重クローズ前提の設計を避けます
強制送信flush()バッファ内データ通信やログでタイミングを意識します
単一バイトwrite(int)下位8ビット文字単位ではなくバイト単位です
配列出力write(byte[])バイト配列全体大きな配列ではメモリ消費に注意します
部分出力write(byte[], int, int)配列の一部offlenの範囲を確認します
フィルタ処理FilterOutputStream加工済み出力委譲先のOutputStreamを持ちます
大文字変換Character.toUpperCase()ASCII中心の例多言語文字では設計を分けます
接頭辞付与PrefixedOutputStream加工テキスト文字コードを固定すると扱いやすくなります
標準出力System.outコンソール表示PrintStreamとして提供されます
読み書き対比InputStream入力バイト出力側とは方向が逆です
パス管理Pathファイル位置新しいコードではFilesとの併用もあります
ログ出力PrintWriter文字列バイト制御が必要なら出力ストリームを選びます
権限エラーAccessDeniedException書き込み失敗保存先の権限を確認します
上書き保存new FileOutputStream(file)既存ファイル既存内容は置き換わります
追記保存new FileOutputStream(file, true)追加データログ用途では改行も制御します
圧縮エントリZipEntryZip内ファイル同名エントリの重複を避けます
変換境界Charset文字とバイトエンコーディングの不一致が文字化けになります
学習順序OutputStream基礎Javaプログラミングファイル操作から始めると理解しやすくなります

この早見表の中で特に押さえたいのは、OutputStreamがバイナリの流れを抽象化し、各サブクラスが出力先や加工内容を変えている点です。Javaのファイル操作とネットワーク処理は別物に見えますが、出力側は同じwrite()の考え方で整理できます。

💡 Tips: OutputStreamを学ぶときは、出力先、変換、バッファ、クローズの順に分けると理解しやすくなります。テキストかバイナリかを先に決めると、エンコーディングと例外処理の設計も自然に定まりますが、これは押さえたい点です。

OutputStreamの使い方

OutputStreamの基本は、出力先を作り、必要なら変換やバッファを重ね、write()でバイトを書き込む流れです。Javaのプログラミングでは、この流れをtry-with-resourcesで囲むと、例外処理とクローズ漏れをまとめて扱えます。

サンプルコード1:テキストファイルへの書き込み

テキストを保存する場合でも、OutputStreamへ渡す直前にはバイト配列へ変換します。JavaではString#getBytes()StandardCharsets.UTF_8を渡すと、実行環境の既定文字コードに依存しにくくなるのが一般的です。

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class WriteTextFile {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("example.txt")) {
            byte[] data = "こんにちは、Java!".getBytes(StandardCharsets.UTF_8);
            fos.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、example.txtにUTF-8のテキスト「こんにちは、Java!」が保存される状態です。

このコードでは、FileOutputStreamがファイル操作の出力先を表し、byte[]が実際に書き込むバイナリ列になります。try-with-resourcesにより、正常終了でも例外発生時でもclose()が呼ばれるため、リソース管理が簡潔になります。

そのため、初心者がつまずきやすいのはStringをそのまま出力できると思い込む点です。OutputStreamは文字ではなくバイトを扱うため、テキストを扱うコードではエンコーディングを明示するのが現実的です。

サンプルコード2:バイナリファイルへの書き込み

バイナリファイルでは、文字列ではなく任意のバイト列をそのまま保存するのが現実的です。Javaのbyte配列は、画像、音声、独自フォーマットなどの低レベルなファイル操作を理解する入口になります。

import java.io.FileOutputStream;
import java.io.IOException;

public class WriteBinaryFile {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("binary.dat")) {
            byte[] data = {0x01, 0x02, 0x03, 0x04, 0x05};
            fos.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、binary.dat01 02 03 04 05の5バイトが保存される状態です。

この例では、テキストのエンコーディング変換を行わず、配列の内容をそのままwrite(byte[])へ渡しています。バイナリをテキストエディタで開くと読みにくい表示になる場合がありますが、データ自体が壊れているとは限りません。

一方、バイナリを扱うプログラミングでは、ファイル形式の仕様とバイト順が処理結果を左右します。保存した値を確認するときは、通常のエディタではなくバイナリエディタや検査用の読み取りコードを使うと判断しやすくなると整理できます。

サンプルコード3:ネットワーク越しにデータを送る

ネットワーク送信でも、ソケットから取得したOutputStreamへバイトを書き込みます。JavaのSocketは接続先との通信路を表し、getOutputStream()で送信用のストリームを取得できます。

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public class NetworkSend {
    public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 8080);
             OutputStream os = socket.getOutputStream()) {
            byte[] data = "ネットワーク越しにこんにちは".getBytes(StandardCharsets.UTF_8);
            os.write(data);
            os.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、localhost:8080で待機するサーバーへUTF-8のメッセージが送られる状態です。

このコードはサーバー側が待機している前提で動作します。接続先が存在しない、ポートが閉じている、通信が遮断されているといった条件ではIOExceptionが発生するため、ネットワークの例外処理はファイル操作より広い原因を想定します。

そのため、実装パターンとしてよく見るのは、送信前にタイムアウトやプロトコルを決め、送信後にflush()でバッファを流す形です。メッセージ境界が必要な通信では、改行や長さヘッダーなどの約束も合わせて設計すると理解できます。

サンプルコード4:バッファを使った書き込み

小さなデータを何度も出力する場合、BufferedOutputStreamでまとめてから出力先へ渡す構成が使われます。バッファはプログラミング上の書き込み回数と、実際の低レベルなI/O回数を分離する役割を持ちます。

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class BufferedWrite {
    public static void main(String[] args) {
        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("buffered.txt"))) {
            byte[] data = "バッファを使って効率よく書き込みます".getBytes(StandardCharsets.UTF_8);
            bos.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、buffered.txtに「バッファを使って効率よく書き込みます」というテキストが保存される状態です。

この構成では、BufferedOutputStreamFileOutputStreamを内側に持ちます。呼び出し側はbos.write(data)だけを意識すればよく、最終的なファイル出力は内側のストリームへ委譲されます。

ただし、バッファに残ったデータはflush()またはclose()のタイミングで出力先へ反映されますし、これが一つの目安です。途中でプロセスが終了する可能性があるログ処理やネットワーク処理では、送信タイミングを意識すると安全です。

サンプルコード5:複数のOutputStreamをチェーンする

JavaのOutputStreamは、出力先の上に処理を重ねる形で組み合わせられます。ファイルへ出すFileOutputStreamを内側に置き、その外側をBufferedOutputStreamで包むと、出力先とバッファリングを分離できます。

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class ChainedStreamExample {
    public static void main(String[] args) {
        try (OutputStream bos = new BufferedOutputStream(new FileOutputStream("chained_output.txt"))) {
            byte[] data = "OutputStreamをチェーンして使います".getBytes(StandardCharsets.UTF_8);
            bos.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、chained_output.txtに「OutputStreamをチェーンして使います」というテキストが保存される状態です。

この書き方では、外側のBufferedOutputStreamを閉じるだけで内側のFileOutputStreamも閉じられます。二重にclose()を書くより、外側のストリームをtry-with-resourcesで管理するほうが読みやすくなります。

これと同じ考え方は、Zip、Base64、シリアライゼーションにもつながりますが、覚えておくと役立つでしょう。Javaの出力APIは、役割の異なるストリームを重ねることで、ファイル操作やネットワーク処理に加工を追加できる設計です。

OutputStreamの応用例

基本のファイル操作を理解したら、OutputStreamを別のAPIで包む応用へ進めます。Zip圧縮、Base64エンコーディング、シリアライゼーションはいずれも、出力先そのものより「書き込む前の加工」が中心になります。

この考え方を押さえると、Javaのプログラミングでは標準APIの名前が変わっても処理の骨格を見失いにくくなると覚えるとよいでしょう。たとえばJavaエスケープ処理のような文字変換も、最終的にテキストやバイナリへ落とし込む場面では出力ストリームの知識が役立ちます。

サンプルコード6:Zipファイルの作成

Zipファイルを作る場合は、FileOutputStreamZipOutputStreamで包み、Zip内の各ファイルをZipEntryとして追加します。エントリごとにputNextEntry()write()closeEntry()を対応させるのが扱いやすい流れです。

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class CreateZipFile {
    public static void main(String[] args) {
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("example.zip"))) {
            ZipEntry entry1 = new ZipEntry("file1.txt");
            zos.putNextEntry(entry1);
            zos.write("これはファイル1です。".getBytes(StandardCharsets.UTF_8));
            zos.closeEntry();

            ZipEntry entry2 = new ZipEntry("file2.txt");
            zos.putNextEntry(entry2);
            zos.write("これはファイル2です。".getBytes(StandardCharsets.UTF_8));
            zos.closeEntry();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、example.zipの中にfile1.txtfile2.txtが含まれる状態です。

このコードでは、Zipそのものへの書き込みはZipOutputStreamが担当し、最終的な保存先は内側のFileOutputStreamが担当します。Zipの各項目はZipEntryで名前を持つため、通常のファイル操作とは少し違い、アーカイブ内部のパス設計も必要になります。

ただし、Zip内に同じ名前のエントリを追加すると扱いにくいアーカイブになると考えられます。複数ファイルをまとめるプログラミングでは、入力ファイル名の正規化、重複回避、エンコーディングの統一を先に決めておくと安定します。

⚠️ 注意: Zip作成では、ファイル名にユーザー入力をそのまま使うと意図しないパスになる場合があります。保存先の検証や相対パスの制限を行い、Zip Slipと呼ばれる展開時のリスクにも注意すると言えるでしょう。

サンプルコード7:Base64エンコーディング

Base64は、バイナリをASCII中心のテキスト表現へ変換するエンコーディングです。メール、JSON、フォーム送信など、バイナリをそのまま載せにくい場面で利用されます。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class Base64EncodingExample {
    public static void main(String[] args) {
        try (OutputStream os = new FileOutputStream("base64_output.txt");
             OutputStream base64OS = Base64.getEncoder().wrap(os)) {
            base64OS.write("これはテストデータです".getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、base64_output.txtにBase64エンコーディング後のテキストが保存される状態です。

このコードでは、Base64.getEncoder().wrap(os)が既存のOutputStreamを包み、書き込まれたバイトをBase64へ変換します。呼び出し側は変換済みの文字列を自分で組み立てず、ラップされた出力ストリームへ通常どおりwrite()できます。

一方、Base64は暗号化ではありません。内容を読みにくく見せる効果はあっても、復号に相当するデコードは容易なので、秘密情報の保護には暗号化や認証の設計が別途必要になるのが基本です。

サンプルコード8:オブジェクトのシリアライゼーション

シリアライゼーションは、Javaオブジェクトの状態をバイト列として保存する仕組みです。ファイルへ保存する場合はObjectOutputStreamを使い、保存対象のクラスにはSerializableを実装します。

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

結果: 期待される出力は、Personクラスがシリアライゼーション可能な型として定義される状態です。

このクラスでは、Serializableを実装し、serialVersionUIDを明示しています。バージョンが変わる可能性のあるクラスでは、シリアライズされたデータとクラス定義の対応を意識する必要があります。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializeExample {
    public static void main(String[] args) {
        Person person = new Person("山田太郎", 30);

        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、person.serPersonオブジェクトの状態がシリアライゼーションされた形で保存される状態です。

この処理では、ObjectOutputStreamFileOutputStreamを包み、オブジェクトをバイナリ表現へ変換して保存します。テキストではないため、通常のエディタで内容を読む用途には向きません。

ただし、信頼できない入力からデシリアライズする設計はリスクが高くなります。保存形式を外部連携にも使う場合は、JSONやProtocol Buffersなどの明示的なデータ形式を選ぶほうが扱いやすい場面もあるのが目安です。

サンプルコード9:カスタムOutputStreamの作成

独自の出力処理を作りたい場合、OutputStreamを継承してwrite(int)を実装します。公式ドキュメントでも、サブクラスを定義する場合は少なくとも1バイトを書き込むメソッドを提供する必要があるとされています。

import java.io.IOException;
import java.io.OutputStream;

public class CustomOutputStream extends OutputStream {
    @Override
    public void write(int b) throws IOException {
        System.out.print((char) b);
    }
}

結果: 期待される出力は、受け取ったバイトを文字として標準出力へ送るCustomOutputStreamが定義される状態です。

この例は学習用の単純な実装です。実際のプログラミングでは、マルチバイト文字、エンコーディング、例外処理、バッファリングを考慮し、単純な(char) b変換だけで済ませない設計が必要になります。

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class CustomOutputStreamExample {
    public static void main(String[] args) {
        try (OutputStream customOS = new CustomOutputStream()) {
            customOS.write("これはテストです".getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、標準出力へ「これはテストです」に相当する文字列が送られる状態です。

このコードは、独自ストリームを呼び出す側の形を確認するためのものです。カスタム処理が必要なときでも、利用側はOutputStream型として扱えるため、既存コードとの接続がしやすくなります。

注意点と対処法

OutputStreamの注意点は、文字コード、クローズ、例外処理に集中します。Javaではテキストとバイナリの境界が明確なため、テキストを出すならエンコーディング、バイナリを出すならバイト列の仕様を分けて考えますし、ここを基本と考えるとよいでしょう。

そのうえで、リソース管理はtry-with-resourcesを優先します。古いコードではfinallyclose()する形も見られますが、新しく書くJavaプログラミングでは自動クローズを使うほうが漏れを減らせます。

文字エンコーディングに関する注意

テキストを扱うときは、OutputStreamWriterで文字からバイトへの変換を明示できるのがポイントです。StandardCharsets.UTF_8を使うと、文字コード名のタイプミスや環境依存を避けやすくなります。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;

public class EncodingExample {
    public static void main(String[] args) {
        try (OutputStreamWriter osw = new OutputStreamWriter(
                new FileOutputStream("encodingTest.txt"), StandardCharsets.UTF_8)) {
            osw.write("こんにちは、世界!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、encodingTest.txtにUTF-8のテキスト「こんにちは、世界!」が保存される状態です。

この書き方では、文字列を直接OutputStreamWriterへ渡し、内部でUTF-8のバイト列へ変換します。FileOutputStreamへ直接getBytes()を渡す方法と比べると、文字出力としての意図が読み取りやすくなります。

ただし、既存システムがShift_JISなどを要求する場合は、相手側の仕様に合わせますし、ここがポイントです。エンコーディングは好みで選ぶものではなく、保存先や通信先が期待する形式と合わせるものです。

ℹ️ 補足: Javaの文字列は内部表現と外部出力のエンコーディングを分けて考えます。ファイル操作やネットワーク通信で文字化けが起きる場合、入力側と出力側のCharsetが一致しているかを確認します。

ストリームのクローズについて

ストリームを閉じないまま処理を終えると、OSリソースが残ったり、バッファ内のデータが出力先へ届かなかったりする場合があるのが一般的です。JavaではAutoCloseableに対応したストリームをtry-with-resourcesで管理できます。

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class CloseStreamExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("closeStream.txt")) {
            fos.write("重要なデータ".getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、closeStream.txtに「重要なデータ」というテキストが保存され、処理終了時にストリームが閉じられる状態です。

この例では、tryの括弧内で作成したFileOutputStreamが自動的に閉じられます。finallyで手動クローズする古い形より、変数のスコープが狭く、例外処理の見通しも良くなります。

一方、複数のストリームをチェーンしている場合は、基本的に外側のストリームを閉じますが、これは押さえたい点です。外側が内側へclose()を委譲する設計が多いため、重ねた順序を意識するとクローズ漏れを防ぎやすくなります。

例外処理の考え方

ファイル操作やネットワーク処理では、保存先が存在しない、権限が足りない、接続が切れるなど、複数の原因でIOExceptionが発生します。例外処理では、ユーザーに見せる情報とログに残す情報を分ける設計が扱いやすくなるのが現実的です。

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class ExceptionHandlingExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("exceptionHandling.txt")) {
            fos.write("このデータは大切です".getBytes(StandardCharsets.UTF_8));
        } catch (FileNotFoundException e) {
            System.out.println("指定されたファイルを開けませんでした");
        } catch (IOException e) {
            System.out.println("ファイルの書き込みに失敗しました");
        }
    }
}

結果: 期待される出力は、正常時にexceptionHandling.txtへテキストが保存され、失敗時には原因に応じたメッセージが表示される状態です。

このコードでは、FileNotFoundExceptionを先に捕捉し、その後でより広いIOExceptionを扱います。例外の継承関係を意識しないと、個別のcatchが到達不能になるため、狭い例外から順に並べます。

具体的には、書き込み先ディレクトリの存在、ファイル権限、ディスク容量、ネットワーク接続の状態を切り分けますし、これが一つの目安です。例外処理を単なるprintStackTrace()で済ませると、利用者に必要な案内が届きにくくなります。

関連する基礎として、コレクションに保存した出力対象を順に処理するならJava List型の扱いも合わせて理解できます。メタ情報を付けて処理を分岐する設計では、Javaアノテーションの知識が役立つ場合もあると整理できます。

カスタマイズ方法

OutputStreamのカスタマイズでは、独自に継承する方法と、既存ストリームを包む方法があります。Javaのプログラミングでは、出力先を直接変更するより、加工だけを担当するクラスを外側に重ねる設計が扱いやすい場面が多くなります。

これにより、ファイル操作、ネットワーク、Zip、Base64、シリアライゼーションのように用途が変わっても、出力先と加工処理を分離できると理解できます。既存クラスを組み合わせる考え方は、Javaのオーバーライドを学ぶときにも理解しやすくなります。

独自のOutputStreamクラスの作成

独自クラスを作る場合は、OutputStreamを継承し、少なくともwrite(int)を実装します。下の例では、内部に別のOutputStreamを持ち、受け取ったASCII文字を大文字に変換してから委譲すると覚えるとよいでしょう。

import java.io.IOException;
import java.io.OutputStream;

public class UppercaseOutputStream extends OutputStream {
    private final OutputStream out;

    public UppercaseOutputStream(OutputStream out) {
        this.out = out;
    }

    @Override
    public void write(int b) throws IOException {
        out.write(Character.toUpperCase((char) b));
    }

    @Override
    public void close() throws IOException {
        out.close();
    }
}

結果: 期待される出力は、受け取った文字を大文字へ変換して内側のOutputStreamへ渡すクラスが定義される状態です。

この実装はASCIIの学習例としては読みやすいものの、日本語などのマルチバイト文字を1バイトずつ変換する用途には向きません。テキスト変換を正確に扱うなら、WriterCharsetを使う設計も検討します。

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class UppercaseOutputExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("test.txt");
             UppercaseOutputStream uos = new UppercaseOutputStream(fos)) {
            uos.write("Hello World".getBytes(StandardCharsets.US_ASCII));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、test.txtに「HELLO WORLD」が保存される状態です。

このように加工用ストリームを外側に置くと、保存先がファイルでもネットワークでも同じ変換を再利用できます。処理を固定のファイル操作へ埋め込まないため、テストや差し替えも行いやすくなります。

既存のOutputStreamの拡張

既存の出力ストリームに機能を足すなら、FilterOutputStreamを継承する方法があると考えられます。FilterOutputStreamは委譲先のOutputStreamを持つため、ラッパー型の実装に向いています。

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class PrefixedOutputStream extends FilterOutputStream {
    private final String prefix;

    public PrefixedOutputStream(OutputStream out, String prefix) {
        super(out);
        this.prefix = prefix;
    }

    @Override
    public void write(byte[] b) throws IOException {
        byte[] prefixed = (prefix + new String(b, StandardCharsets.UTF_8)).getBytes(StandardCharsets.UTF_8);
        super.write(prefixed);
    }
}

結果: 期待される出力は、渡されたバイト列をUTF-8のテキストとして読み、先頭に接頭辞を付けて出力するクラスが定義される状態です。

この例では、エンコーディングをStandardCharsets.UTF_8でそろえています。入力バイトを文字列に戻す処理があるため、バイナリ全般に使うのではなく、テキスト専用のラッパーとして扱うのが自然です。

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class PrefixedOutputExample {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("prefixed.txt");
             PrefixedOutputStream pos = new PrefixedOutputStream(fos, "PREFIX: ")) {
            pos.write("data here".getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

結果: 期待される出力は、prefixed.txtに「PREFIX: data here」というテキストが保存される状態です。

このようなカスタマイズは、ログの接頭辞、監査用のタグ付け、簡単な変換処理に応用できます。ただし、複雑なテキスト整形をすべてOutputStreamへ詰め込むと責務が広がるため、変換処理と出力処理の境界を分けると保守しやすくなります。

条件判定を伴う出力名の生成では、日付や暦の扱いも関わりますが、覚えておくと役立つでしょう。ファイル名に年や月を入れる設計なら、Javaでうるう年を判定する考え方も参考になります。

まとめ

JavaのOutputStreamは、テキスト、バイナリ、ファイル操作、ネットワーク、Zip、Base64、シリアライゼーションをつなぐ出力APIです。出力先が変わっても、バイトを書き込むという中心の考え方は共通しています。

そのため、プログラミングで最初に整理したいのは、どこへ出すのか、何をバイトへ変換するのか、どのタイミングでflush()close()を行うのかという点です。テキストならエンコーディング、バイナリなら形式仕様、ネットワークなら相手側のプロトコルも合わせて確認すると言えるでしょう。

これらを押さえると、FileOutputStreamだけでなく、BufferedOutputStreamZipOutputStreamObjectOutputStreamFilterOutputStreamも同じ流れで読めるようになります。Javaの出力処理は、単体のクラスを暗記するより、ストリームを重ねる構造として理解するほうが実装に結びつきます。

例外処理ではIOExceptionを雑に握りつぶさず、ファイルがないのか、権限がないのか、ネットワークが切れたのかを切り分けますし、ここを基本と考えるとよいでしょう。出力コードは成功時だけでなく、失敗時に利用者やログへ何を伝えるかまで含めて設計すると、運用時の調査もしやすくなります。

OutputStreamを軸に学ぶと、Javaの標準ライブラリが同じ設計思想でつながっていることが見えてきます。ファイル操作から始め、バイナリ、Zip、Base64、シリアライゼーション、カスタムストリームへ広げる流れで身につけると、出力処理の見通しが大きく変わりますし、ここがポイントです。

関連記事

著者: Japanシーモア編集部

Japanシーモアは、Web/IoT/APP/SYS 分野のプログラミング情報を体系的に提供するメディアです。本記事は編集部による執筆とAI支援を組み合わせて制作し、公開前に編集部が校正しています。誤りや改善案がございましたらお問い合わせよりご連絡ください。

※本記事は実在のエンジニア複数名で構成される Japanシーモア編集部が、AI支援を活用して作成・校正・公開しています。