JavaのOutputStream!9つの実践サンプルで完全理解

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

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

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

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

基本的な知識があればサンプルコードを活用して機能追加、目的を達成できるように作ってあります。

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

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

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

はじめに

データを出力することはプログラミングの日常業務でよく行われますが、正確に何をしているのか、どういう仕組みで動いているのかを理解していないと、効率的なコードを書くことは難しいです。

そこで今回は、Java言語におけるデータ出力の基本である「OutputStream」にスポットを当て、その全貌を明らかにします。

基本的な使い方から応用例、そして避けては通れない「注意点」まで、9つの実用的なサンプルコードを交えながら解説します。

●Javaとは

Javaという名前を聞いて、多くの人は「それってプログラミング言語でしょ?」と即答できるかもしれません。

ですが、Javaとは一体何なのでしょうか。

○プログラミング言語の一つである理由

Javaは、1990年代にSun Microsystems(現在はOracle Corporationが所有)によって開発されたプログラミング言語です。

その主な特長として「Write Once, Run Anywhere(一度書いて、どこでも実行)」があります。

これはJavaがプラットフォーム非依存であるという事実を端的に表しています。

つまり、Javaで書かれたプログラムは、異なるオペレーティングシステムであっても、Java Virtual Machine(JVM)がインストールされていれば、特に修正を加えずに動作させることができます。

○Javaの特徴

Javaのもう一つの特長は、豊富な標準ライブラリです。

これによってファイル操作、ネットワーク通信、データベース接続など、多くの機能を手軽に実装することができます。

また、Javaはオブジェクト指向プログラミングを完全にサポートしているため、設計が行いやすく、大規模なアプリケーション開発にも適しています。

このような特長を持つJavaは、ウェブアプリケーションから組み込みシステム、ビッグデータ解析に至るまで幅広い用途で使用されています。

●OutputStreamとは

OutputStreamというのは、Javaにおいてデータを出力するための基礎となるクラスです。

データを出力とは、要するにメモリ上のデータをファイル、ネットワーク、あるいは他のデータストレージへと送り出すことです。

○基本的な説明

Javaでの出力処理を一言で言うならば、OutputStreamクラスか、そのサブクラスを使用するというのが基本的なフローです。

このOutputStreamクラスは、java.ioパッケージに位置しています。このクラスは抽象クラスであり、その実装はサブクラスによって行われます。

よく使用されるサブクラスとしてはFileOutputStream(ファイルへの出力)、ByteArrayOutputStream(バイト配列への出力)、BufferedOutputStream(バッファを利用した効率的な出力)などがあります。

○OutputStreamの役割

OutputStreamの主な役割は、データをバイト形式で書き出すことです。

JavaではテキストデータはString、数値データはintdoubleといった形で扱いますが、最終的にこれらを外部に出力する際にはバイトデータに変換して送り出します。

その役割を担っているのがOutputStreamです。

このOutputStreamを使って具体的に何ができるかと言えば、ファイルにデータを保存したり、ネットワーク経由でデータを送信したりすることができます。

さらに、プラグインのような形で機能を追加することも可能で、その場合は独自のOutputStreamクラスを作成して、既存のものと「チェーン」する形で使用することが多いです。

●OutputStreamの使い方

OutputStreamの使い方を具体的に解説していきます。

この部分では、具体的なサンプルコードとその説明を交えて、実際にどのようにOutputStreamを使うのかを深く探っていきます。

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

Javaで最も基本的なOutputStreamの使い方といえば、テキストファイルへのデータの書き込みです。

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

public class WriteTextFile {
    public static void main(String[] args) {
        try (FileOutputStream fos = new FileOutputStream("example.txt")) {
            // 文字列をバイト配列に変換
            byte[] data = "こんにちは、Java!".getBytes();

            // ファイルに書き込む
            fos.write(data);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このコードではFileOutputStreamクラスを用いてテキストファイルにデータを書き込んでいます。

try-with-resources文を用いることで、自動的にリソースの解放が行われます。

getBytes()メソッドで文字列をバイト配列に変換後、writeメソッドでファイルに書き込んでいます。

このコードを実行すると、「example.txt」という名前のテキストファイルが生成され、その中に「こんにちは、Java!」というテキストが書き込まれます。

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

テキスト以外のデータ、特にバイナリデータもOutputStreamで扱うことができます。

例えば、画像ファイルや音声ファイルなどが該当します。

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.datという名前のバイナリファイルに5バイトのデータを書き込んでいます。

byte[] data配列に格納されたバイナリデータが、writeメソッドによってファイルに書き込まれます。

このコードを実行した後に生成されたbinary.datファイルをバイナリエディタで開くと、01 02 03 04 05というバイトデータが格納されていることが確認できます。

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

ネットワークを通じてデータを送る際にもOutputStreamは活躍します。

Javaの標準ライブラリにはSocketクラスが存在し、このクラスを使用してネットワーク通信が行えます。

このソケットから取得したOutputStreamを使用してデータを送信する例を紹介します。

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class NetworkSend {
    public static void main(String[] args) {
        try {
            // ソケットの作成
            Socket socket = new Socket("localhost", 8080);
            // OutputStreamの取得
            OutputStream os = socket.getOutputStream();

            // 送信するデータ
            byte[] data = "ネットワーク越しにこんにちは".getBytes();

            // データの送信
            os.write(data);

            // リソースの解放
            os.close();
            socket.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このコードでは、まずSocketクラスを使ってローカルホスト(自分自身)の8080ポートに接続しています。

次に、socket.getOutputStream()でソケットに関連付けられたOutputStreamを取得します。

ここで取得したOutputStreamに書き込むことで、ネットワーク越しにデータを送ることができます。

このコードを実行すると、8080ポートで待機しているサーバーに「ネットワーク越しにこんにちは」というメッセージが送信されます。

もちろん、サーバー側がこのポートで待機している状態である必要があります。

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

大量のデータを書き込む必要がある場合、バッファを使った書き込みが効率的です。

Javaには、このためのBufferedOutputStreamクラスが用意されています。

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

public class BufferedWrite {
    public static void main(String[] args) {
        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("buffered.txt"))) {
            // 書き込むデータ
            byte[] data = "バッファを使って効率よく書き込みます".getBytes();

            // データの書き込み
            bos.write(data);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このコードではBufferedOutputStreamFileOutputStreamにラップして使用しています。

バッファを経由してデータが書き込まれるため、多数の小さな書き込み操作が大きなブロックとしてまとめられ、ディスクへの書き込み回数が減少します。

このコードを実行すると、「buffered.txt」というファイルが作成され、その中に「バッファを使って効率よく書き込みます」という文字列が書き込まれます。

但し、視覚的な違いはありませんが、内部的には効率的に書き込みが行われています。

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

JavaのOutputStreamは非常に柔軟で、一つのOutputStreamに別のOutputStreamを「チェーン」して、複合的な出力処理を実現できます。

このテクニックは特にファイル出力やネットワーク通信などで非常に役立つものです。

次に、FileOutputStreamBufferedOutputStreamをチェーンする例を紹介します。

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class ChainedStreamExample {
    public static void main(String[] args) {
        try {
            // FileOutputStreamのインスタンス作成
            OutputStream fos = new FileOutputStream("chained_output.txt");

            // BufferedOutputStreamでラップ
            OutputStream bos = new BufferedOutputStream(fos);

            // 書き込むデータ
            byte[] data = "OutputStreamをチェーンして使います".getBytes();

            // データの書き込み
            bos.write(data);

            // リソースの解放
            bos.close();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

この例では、FileOutputStreamのインスタンス(fos)を作成した後に、それをBufferedOutputStreambos)でラップしています。

この構成によって、BufferedOutputStreamの効率的なバッファリング能力とFileOutputStreamのファイル書き込み機能が同時に利用できるようになります。

データはまずBufferedOutputStreamに書き込まれ、その後FileOutputStreamを通じてファイルに書き込まれます。

このコードを実行すると、「chained_output.txt」という名前のテキストファイルが生成され、その中に「OutputStreamをチェーンして使います」という文字列が書き込まれます。

この方法により、一つのOutputStreamを基本に、別のOutputStreamを連結することで、複数の機能を簡単に組み合わせられます。

●OutputStreamの応用例

OutputStreamの基本的な使い方を把握したところで、次に進んでいくつかの応用例を見てみましょう。

OutputStreamは非常に多機能であり、様々なシチュエーションでその能力を発揮します。

ここでは、Zipファイルの作成とBase64エンコーディングについて、詳細なサンプルコードを交えながら説明します。

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

Javaにはjava.util.zip.ZipOutputStreamという便利なクラスがあり、これを使ってZip形式の圧縮ファイルを作成できます。

このクラスはOutputStreamを継承しているため、通常のOutputStreamと同様に操作できます。

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class CreateZipFile {
    public static void main(String[] args) {
        try {
            // ZipOutputStreamのインスタンスを作成
            FileOutputStream fos = new FileOutputStream("example.zip");
            ZipOutputStream zos = new ZipOutputStream(fos);

            // 圧縮するエントリを追加
            ZipEntry entry1 = new ZipEntry("file1.txt");
            zos.putNextEntry(entry1);
            zos.write("これはファイル1です。".getBytes());
            zos.closeEntry();

            // 別のエントリを追加
            ZipEntry entry2 = new ZipEntry("file2.txt");
            zos.putNextEntry(entry2);
            zos.write("これはファイル2です。".getBytes());
            zos.closeEntry();

            // リソースを解放
            zos.close();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このコードでは、まずFileOutputStreamのインスタンスを生成してexample.zipという名前のZipファイルを作ります。

その後、このFileOutputStreamZipOutputStreamでラップします。

次に、ZipEntryオブジェクトを作成して、それをputNextEntry()メソッドでZipファイルに追加します。

最後にwrite()メソッドで実際のデータを書き込んで、closeEntry()でエントリの追加を完了します。

コードの実行後にはexample.zipというZipファイルが生成され、その中にはfile1.txtfile2.txtが含まれています。

それぞれのファイルには「これはファイル1です。」および「これはファイル2です。」という内容が書き込まれています。

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

Base64エンコーディングは、バイナリデータをテキスト形式で安全に転送するためのエンコーディング方法です。

Java8以降ではjava.util.Base64クラスが用意されており、OutputStreamをラップする形で簡単にBase64エンコーディングができます。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Base64;

public class Base64EncodingExample {
    public static void main(String[] args) {
        try {
            // FileOutputStreamで出力先を設定
            OutputStream os = new FileOutputStream("base64_output.txt");

            // Base64でエンコード
            OutputStream base64OS = Base64.getEncoder().wrap(os);

            // データの書き込み
            base64OS.write("これはテストデータです".getBytes());

            // リソースの解放
            base64OS.close();
            os.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このコードはFileOutputStreamを作成して出力先のテキストファイル(base64_output.txt)を指定します。

その後、Base64.getEncoder().wrap()メソッドを使用して、このOutputStreamをBase64エンコーディング用のOutputStreamでラップします。

最後に、このラップされたOutputStreamにデータを書き込むことで、Base64エンコーディングされた内容がテキストファイルに保存されます。

このコードを実行すると、base64_output.txtという名前のテキストファイルが生成されます。

このファイルを開いてみると、Base64エンコーディングされた「これはテストデータです」という文字列が保存されています。

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

Javaプログラミングにおいて、オブジェクトの状態を一時的に保存したり、ネットワーク越しに送ったりする場面は少なくありません。

このようなケースで役立つのがオブジェクトのシリアライゼーションです。

Javaにはjava.io.ObjectOutputStreamというクラスが用意されており、これを使ってオブジェクトをOutputStreamに書き込むことができます。

まず、シリアライズ可能なクラスを作成する必要があります。

そのためにはSerializableインターフェースを実装する必要があります。

import java.io.Serializable;

// Serializableインターフェースを実装
public class Person implements Serializable {
    private String name;
    private int age;

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

    // getterとsetterは省略
}

このクラスをシリアライズしてファイルに保存するコードを見てみましょう。

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 {
            // ファイルにオブジェクトを保存するためのストリームを作成
            FileOutputStream fos = new FileOutputStream("person.ser");
            ObjectOutputStream oos = new ObjectOutputStream(fos);

            // オブジェクトをシリアライズして保存
            oos.writeObject(person);

            // ストリームを閉じる
            oos.close();
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このコードでは、最初にシリアライズするPersonオブジェクトを作成しています。

次にFileOutputStreamでシリアライズ後のデータを保存するファイル(person.ser)を指定し、その後にObjectOutputStreamを使って実際にオブジェクトをシリアライズしています。

実行すると、person.serという名前のファイルが生成され、この中にPersonオブジェクトがシリアライズされた状態で保存されています。

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

Javaでは、独自のOutputStreamを作成することも可能です。

OutputStreamを継承したクラスを新しく作成し、必要なメソッドをオーバーライドすることで実現できます。

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);
    }
}

このカスタムなOutputStreamは非常に単純で、writeメソッドが呼び出された際に標準出力にその値を出力します。

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

このコードを実行すると、コンソールに「これはテストです」と出力されます。

●注意点と対処法

OutputStreamを使う上で気をつけるべき点はいくつか存在します。

具体的な問題点とその対処法を見ていきましょう。

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

Javaでは、OutputStreamを使ってテキストデータを書き込む場合にもエンコーディングが関わってきます。

具体的には、OutputStreamWriterなどを使用する際に、エンコーディングの指定が必要な場合があります。

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 {
            // FileOutputStreamの生成
            FileOutputStream fos = new FileOutputStream("encodingTest.txt");
            // エンコーディングをUTF-8で指定して、OutputStreamWriterを生成
            OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);

            // テキストデータの書き込み
            osw.write("こんにちは、世界!");
            osw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このコードでは、OutputStreamWriterを生成する際にStandardCharsets.UTF_8を指定しています。

これにより、テキストデータがUTF-8形式でエンコーディングされることが保証されます。

このコードを実行すると、encodingTest.txtというテキストファイルが生成され、その中にUTF-8形式で「こんにちは、世界!」と書かれたテキストが保存されます。

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

OutputStreamを使用した後は、必ずストリームをクローズする必要があります。

そうしないと、リソースのリーク(無駄なリソースの占有)が発生する可能性があります。

try {
    FileOutputStream fos = new FileOutputStream("closeStream.txt");
    fos.write("重要なデータ".getBytes());
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 必ずfinallyブロックでストリームをクローズする
    if (fos != null) {
        try {
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

このコードでは、finallyブロック内でfos.close();として、OutputStreamを明示的にクローズしています。

○例外処理のベストプラクティス

OutputStreamを使用する際には、様々な例外が発生する可能性があります。

特にIOExceptionは、ファイルの読み書きやネットワーク通信などで頻繁に見られます。

このような例外が発生した場合の対処法としては、適切な例外処理を行うことが不可欠です。

try {
    FileOutputStream fos = new FileOutputStream("exceptionHandling.txt");
    fos.write("このデータは大切です".getBytes());
} catch (FileNotFoundException e) {
    System.out.println("指定されたファイルが見つかりませんでした");
} catch (IOException e) {
    System.out.println("ファイルの書き込みに失敗しました");
} finally {
    if (fos != null) {
        try {
            fos.close();
        } catch (IOException e) {
            System.out.println("ファイルのクローズに失敗しました");
        }
    }
}

このコードでは、try-catch-finallyブロックを使って、各種の例外に対処しています。

FileNotFoundExceptionIOExceptionに対してそれぞれ個別の対処を行っており、エラーメッセージを出力しています。

●カスタマイズ方法

JavaのOutputStreamは柔軟にカスタマイズ可能です。

カスタマイズする主な方法は、基本的に二つあります。

一つは独自のOutputStreamクラスを作成する方法、もう一つは既存のOutputStreamを拡張する方法です。

○独自のOutputStreamクラスの作成

Javaにおいて、独自のOutputStreamクラスを作成する際は、OutputStreamクラスを継承して必要なメソッドをオーバーライドする方法が一般的です。

下記のサンプルコードは、全ての出力を大文字に変換するカスタムOutputStreamを表しています。

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

public class UppercaseOutputStream extends OutputStream {
    private OutputStream out;

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

    // 出力を大文字に変換する
    @Override
    public void write(int b) throws IOException {
        out.write(Character.toUpperCase(b));
    }
}

この独自のOutputStreamクラスでは、write(int b)メソッドをオーバーライドしています。

このメソッド内で、引数として渡された整数(ASCIIコード)を大文字に変換してから、内部のOutputStream(out)に渡しています。

このカスタムOutputStreamを使用すると、次のように全ての出力が大文字になります。

try (FileOutputStream fos = new FileOutputStream("test.txt");
     UppercaseOutputStream uos = new UppercaseOutputStream(fos)) {
    uos.write("Hello World".getBytes());
}

このコードを実行すると、test.txtという名前のファイルが作成され、その中身は「HELLO WORLD」と全て大文字で出力されます。

○既存のOutputStreamの拡張

既存のOutputStreamに機能を追加したい場合、FilterOutputStreamクラスを使用する方法もあります。

下記のサンプルコードは、出力データに指定したプレフィックスを追加するFilterOutputStreamの例です。

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

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)).getBytes();
        super.write(prefixed);
    }
}

このPrefixedOutputStreamクラスでは、write(byte[] b)メソッドをオーバーライドしています。

渡されたバイト配列にプレフィックスを追加してから、スーパークラスのwriteメソッドを呼び出しています。

このPrefixedOutputStreamを使って出力を行うと、次のように出力データの先頭にプレフィックスが追加されます。

try (FileOutputStream fos = new FileOutputStream("prefixed.txt");
     PrefixedOutputStream pos = new PrefixedOutputStream(fos, "PREFIX: ")) {
    pos.write("data here".getBytes());
}

このコードを実行すると、prefixed.txtというファイルが生成され、その中身は「PREFIX: data here」という形でプレフィックスが追加されたテキストが保存されます。

まとめ

これまでにわたって、JavaのOutputStreamについて幅広く深く解説してきました。

JavaのOutputStreamは多機能でありながら柔軟性も高いため、その使い方や応用範囲は非常に広いです。

しかし、その機能を最大限に活用するには、基本的な操作から応用テクニック、さらにはカスタマイズまで、一通りの知識とスキルが必要です。

この記事が、OutputStreamに関する疑問を解消し、より一層の理解を深める一助になれば幸いです。

それでは、皆さまのプログラミングがより楽しく、より効果的に進むことを願っています。

お読みいただき、誠にありがとうございました。