サイトアイコン Omomuki Tech

Java I/Oの基礎を徹底解説!java.ioパッケージの完全ガイド

この記事から得られる知識

  • Javaのjava.ioパッケージの基本的な概念と全体像
  • バイトストリームとキャラクタストリームの違いと、それぞれの適切な使い分け
  • ファイル操作、特に読み書きを効率的に行うためのバッファリングの重要性
  • Fileクラスを使ったファイルやディレクトリの操作方法
  • Javaオブジェクトを永続化するためのシリアライズの仕組み
  • モダンなI/O APIであるjava.nioとの基本的な違い

はじめに:Java入出力の世界へようこそ

Javaプログラミングにおいて、ファイル操作やネットワーク通信など、プログラムの外部とデータをやり取りする「入出力(I/O)」は避けては通れない重要な機能です。その中核を担うのがjava.ioパッケージです。このパッケージは、Javaの初期バージョンから提供されている伝統的なI/Oライブラリであり、今なお多くの場面で利用されています。

この記事では、java.ioパッケージの基本的な概念から、主要なクラスの使い方、さらにはより効率的なデータ操作のテクニックまで、詳細にわたって解説していきます。初心者の方にも理解しやすいように、具体的なコード例を交えながら、一歩一歩進めていきましょう。


第1章: Java I/Oの基本概念

java.ioを理解する上で最も重要な概念が「ストリーム」です。ストリームとは、データの流れを抽象化したものです。プログラムは、キーボード、ファイル、ネットワークなど、様々な入出力先をすべてこの「ストリーム」という統一されたモデルで扱います。これにより、データの供給元や出力先が何であれ、同じような方法でプログラミングできるのです。

java.ioのストリームは、大きく分けて2種類に分類されます。

1. バイトストリーム (Byte Streams)

データをバイト単位 (8bit)で扱うストリームです。画像、音声、動画、実行可能ファイルなど、あらゆる種類のバイナリデータを処理するのに適しています。テキストデータも扱えますが、文字エンコーディングを意識した処理は行いません。

  • 入力: InputStream (基底クラス)
  • 出力: OutputStream (基底クラス)

2. キャラクタストリーム (Character Streams)

データを文字単位 (16bit Unicode)で扱うストリームです。テキストデータの処理に特化しており、内部で自動的に文字エンコーディングの変換を行ってくれます。日本語のようなマルチバイト文字を扱う場合は、こちらを使用するのが一般的です。

  • 入力: Reader (基底クラス)
  • 出力: Writer (基底クラス)

どちらのストリームを選ぶかは、扱うデータの内容によって決まります。テキストファイルならキャラクタストリーム、それ以外のバイナリデータならバイトストリーム、と覚えておくと良いでしょう。


第2章: バイトストリームを極める (InputStream / OutputStream)

バイトストリームは、すべてのデータの基本であるバイトを直接操作します。ここでは、ファイルへの入出力を例に、その使い方を見ていきましょう。

ファイル入出力の基本: FileInputStreamとFileOutputStream

FileInputStreamはファイルからデータをバイト単位で読み込むためのクラス、FileOutputStreamはファイルへデータをバイト単位で書き込むためのクラスです。

ここで非常に重要になるのが、リソースの解放です。ファイルやネットワーク接続などのリソースは、使い終わったら必ず閉じる(closeする)必要があります。Java 7以降では、try-with-resources文を使うことで、このクローズ処理を自動化でき、コードが簡潔かつ安全になります。

try-with-resources文のすすめ
try-with-resources文は、tryキーワードの後の括弧内でリソース(AutoCloseableインタフェースを実装したオブジェクト)を宣言します。この構文を使うと、tryブロックが終了する際に、宣言されたリソースのclose()メソッドが自動的に呼び出されます。これにより、finallyブロックで明示的にclose()を呼び出す必要がなくなり、リソースの解放漏れを防ぐことができます。

コード例: ファイルのコピー

以下のコードは、input.jpgというファイルをoutput.jpgという名前でコピーする例です。

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

public class FileCopyExample {
    public static void main(String[] args) {
        String inputFile = "input.jpg";
        String outputFile = "output.jpg";

        // try-with-resources文でリソースを自動的にクローズ
        try (FileInputStream in = new FileInputStream(inputFile);
             FileOutputStream out = new FileOutputStream(outputFile)) {

            int byteData;
            // 1バイトずつ読み込み、ファイルの終端に達するまで繰り返す (-1が終端)
            while ((byteData = in.read()) != -1) {
                // 読み込んだ1バイトを書き込む
                out.write(byteData);
            }
            System.out.println("ファイルのコピーが完了しました。");

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

高速化の鍵: BufferedInputStreamとBufferedOutputStream

上記の例では、1バイトずつ読み書きを行っています。これはディスクへのアクセスが頻繁に発生するため、特に大きなファイルを扱う場合に非常に非効率です。

この問題を解決するのがバッファリングです。BufferedInputStreamBufferedOutputStreamは、内部に「バッファ」と呼ばれるメモリ領域を持ちます。読み込み時はデータをある程度の塊(ブロック単位)でまとめてバッファに読み込み、プログラムはそのバッファからデータを取得します。書き込み時も同様に、データを一旦バッファに貯めておき、バッファが一杯になったら実際のファイルに一括で書き込みます。これにより、ディスクアクセスの回数が劇的に減り、パフォーマンスが大幅に向上します。

コード例: バッファリングを使った高速なファイルコピー

既存のストリームをコンストラクタでラップする(包む)だけで、簡単にバッファリング機能を追加できます。

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

public class BufferedFileCopyExample {
    public static void main(String[] args) {
        String inputFile = "large_input.zip";
        String outputFile = "large_output.zip";

        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(inputFile));
             BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile))) {

            byte[] buffer = new byte; // 8KBのバッファ
            int length;
            // バッファにまとめて読み込み、読み込んだバイト数(length)が-1になるまで繰り返す
            while ((length = in.read(buffer)) != -1) {
                // バッファの内容を、読み込んだバイト数分だけ書き込む
                out.write(buffer, 0, length);
            }
            System.out.println("バッファリングによる高速なファイルコピーが完了しました。");

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

このコードでは、8192バイト(8KB)のバイト配列をバッファとして使用し、read(byte[] buffer)メソッドでデータをまとめて読み込んでいます。この方法は、1バイトずつ読み書きするよりもはるかに高速です。


第3章: キャラクタストリームを使いこなす (Reader / Writer)

テキストファイルを扱う際には、キャラクタストリームの出番です。キャラクタストリームは文字コードを意識した処理を行ってくれるため、文字化けなどの問題を回避しやすくなります。

テキストファイルの読み書き: FileReaderとFileWriter

FileReaderFileWriterは、テキストファイルの読み書きを簡単に行うためのクラスです。ただし、これらのクラスはOSのデフォルトの文字コードを使用するため、環境によっては意図しない文字化けが発生する可能性があります。

文字コードを指定する: InputStreamReaderとOutputStreamWriter

文字化けを防ぎ、特定の文字コード(例: UTF-8, Shift_JIS)でファイルを読み書きしたい場合は、InputStreamReaderOutputStreamWriterを使用します。これらのクラスは、バイトストリームとキャラクタストリームの間の「橋渡し」の役割を果たします。

行単位の処理が得意: BufferedReaderとBufferedWriter

バイトストリームと同様に、キャラクタストリームにもバッファリングを行うクラスがあります。BufferedReaderBufferedWriterです。

特にBufferedReaderは、テキスト処理で非常によく使われるreadLine()メソッドを提供しています。このメソッドを使うと、ファイルから1行ずつ文字列を読み込むことができ、非常に便利です。

コード例: UTF-8でテキストファイルを読み書きする

以下のコードは、input.txtをUTF-8として読み込み、その内容に追記してoutput.txtにUTF-8で保存する例です。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;

public class CharacterStreamExample {
    public static void main(String[] args) {
        String inputFile = "input.txt";
        String outputFile = "output.txt";

        try (BufferedReader reader = new BufferedReader(
                                        new InputStreamReader(
                                            new FileInputStream(inputFile), StandardCharsets.UTF_8));
             BufferedWriter writer = new BufferedWriter(
                                        new OutputStreamWriter(
                                            new FileOutputStream(outputFile), StandardCharsets.UTF_8))) {

            String line;
            // 1行ずつ読み込み、ファイルの終端(null)まで繰り返す
            while ((line = reader.readLine()) != null) {
                // 読み込んだ行を書き込む
                writer.write(line);
                // 改行を書き込む
                writer.newLine();
            }

            // 新しい行を追記する
            writer.write("--- ここから追記 ---");
            writer.newLine();
            writer.write("この行はプログラムによって追加されました。");
            writer.newLine();

            System.out.println("テキストファイルの処理が完了しました。");

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

この例のように、ストリームクラスはコンストラクタで別のストリームを受け取ることで、機能を「デコレート(装飾)」していくことができます。new BufferedReader(new InputStreamReader(new FileInputStream(...)))という構造は、java.ioの典型的な使い方です。


第4章: ファイル操作の要 `java.io.File`クラス

これまでファイルの「内容」を扱うストリームを見てきましたが、ファイルやディレクトリ「そのもの」を操作するにはjava.io.Fileクラスを使用します。

重要な注意点として、Fileオブジェクトはファイルシステム上のパスを表すものであり、ファイルの内容そのものではないということを理解しておく必要があります。

`File`クラスの主なメソッド

メソッド 説明
createNewFile() 新しい空のファイルを作成します。成功すればtrueを返します。
mkdir() このパス名が示すディレクトリを作成します。
mkdirs() このパス名が示すディレクトリを作成します。必要な親ディレクトリも一緒に作成します。
exists() ファイルまたはディレクトリが存在するかどうかを確認します。
isFile() これがファイルであるかどうかを確認します。
isDirectory() これがディレクトリであるかどうかを確認します。
getName() ファイルまたはディレクトリの名前を返します。
getPath() コンストラクタで指定されたパス文字列を返します。
getAbsolutePath() 絶対パスを返します。
length() ファイルのサイズをバイト単位で返します。
list() ディレクトリ内のファイルとディレクトリの名前の配列を返します。
listFiles() ディレクトリ内のファイルとディレクトリのFileオブジェクトの配列を返します。
delete() ファイルまたは空のディレクトリを削除します。
renameTo(File dest) ファイルまたはディレクトリの名前を変更します(移動も可能)。

コード例: `File`クラスの基本的な使い方

import java.io.File;
import java.io.IOException;

public class FileClassExample {
    public static void main(String[] args) {
        // ディレクトリを表すFileオブジェクトを作成
        File dir = new File("mydir/test");

        // 親ディレクトリも含めてディレクトリを作成
        if (dir.mkdirs()) {
            System.out.println("ディレクトリを作成しました: " + dir.getAbsolutePath());
        }

        // ファイルを表すFileオブジェクトを作成
        File file = new File(dir, "myfile.txt");

        try {
            // ファイルの存在を確認
            if (file.exists()) {
                System.out.println("ファイルは既に存在します。");
            } else {
                // 新しいファイルを作成
                if (file.createNewFile()) {
                    System.out.println("ファイルを作成しました: " + file.getName());
                }
            }

            // ファイル情報の表示
            System.out.println("絶対パス: " + file.getAbsolutePath());
            System.out.println("ファイルサイズ: " + file.length() + " バイト");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
注意: `java.nio.file.Path`への移行
Java 7で導入された新しいI/O API(NIO.2)では、java.io.Fileに代わるものとしてjava.nio.file.Pathインタフェースと、ユーティリティクラスjava.nio.file.Filesが提供されています。これらは、例外処理の改善、シンボリックリンクのサポート、より機能的なファイル操作メソッドなど、多くの点でFileクラスよりも優れています。新規に開発を行う場合は、NIO.2の使用を検討することをお勧めします。

第5章: 発展的なトピック

オブジェクトの直列化(シリアライズ)

java.ioの強力な機能の一つに、オブジェクトの直列化(Serialization)があります。これは、メモリ上にあるJavaオブジェクトの状態を、ファイルに保存したりネットワーク経由で送信したりできるバイト列に変換するプロセスです。そして、そのバイト列から元のオブジェクトを復元することを非直列化(Deserialization)と呼びます。

オブジェクトを直列化可能にするには、そのクラスにjava.io.Serializableインタフェースを実装(implements)するだけです。このインタフェースはメソッドを何も持たない「マーカーインタフェース」であり、JVMに対してこのクラスのオブジェクトが直列化可能であることを示す役割を果たします。

オブジェクトの直列化にはObjectOutputStream、非直列化にはObjectInputStreamを使用します。

コード例: オブジェクトの保存と復元

import java.io.Serializable;
import java.io.ObjectOutputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.FileInputStream;
import java.io.IOException;

// 直列化可能にするためにSerializableを実装
class User implements Serializable {
    private static final long serialVersionUID = 1L; // クラスのバージョンを管理するためのID
    String name;
    int age;
    // transientキーワードを付けると、そのフィールドは直列化の対象外になる
    transient String password;

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

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + ", age=" + age + ", password='" + password + '\'' + '}';
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        User user = new User("Taro Yamada", 30, "mySecret");
        String filename = "user.ser";

        // --- 1. オブジェクトをファイルに直列化 ---
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filename))) {
            oos.writeObject(user);
            System.out.println("オブジェクトを保存しました: " + user);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // --- 2. ファイルからオブジェクトを非直列化 ---
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
            User restoredUser = (User) ois.readObject();
            System.out.println("オブジェクトを復元しました: " + restoredUser);
            // passwordはtransientだったので復元されずnullになる
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

第6章: java.ioとjava.nioの違い

Javaには、java.ioの他にもう一つの主要なI/O APIがあります。それがJava 4から導入されたjava.nio(New I/O)です。両者には設計思想に根本的な違いがあります。

特徴 java.io java.nio
指向性 ストリーム指向
一方向のデータの流れ。読み取りと書き込みが別々のストリーム。
バッファ指向
データを一度バッファに読み込み、そのバッファを操作する。双方向のチャネル。
ブロッキング ブロッキングI/O
読み書き処理が完了するまで、スレッドが待機(ブロック)される。
ノンブロッキングI/O対応
データが準備できていなくてもスレッドはブロックされず、他の処理を続行できる。
主な用途 比較的単純な、逐次的なファイルアクセス。小規模なデータ処理。 高性能なネットワークプログラミング、大量のファイル処理、サーバーサイドアプリケーション。

簡単に言えば、java.ioはシンプルで扱いやすい一方、java.nioはより高機能でスケーラビリティに優れています。どちらを使うべきかは、アプリケーションの要件によります。単純な設定ファイルの読み書きなどであればjava.ioで十分ですが、高いパフォーマンスが求められるサーバーアプリケーションなどではjava.nioが選択されることが多いです。


まとめ

この記事では、Javaの基本的な入出力ライブラリであるjava.ioパッケージについて、その核心的な概念から実践的な使い方までを幅広く解説しました。

キーポイントの再確認:
  • データの流れはストリームとして抽象化される。
  • バイナリデータはバイトストリーム(InputStream/OutputStream)、テキストデータはキャラクタストリーム(Reader/Writer)で扱う。
  • パフォーマンス向上のためには、Buffered...クラスを使ったバッファリングが不可欠。
  • ファイルやディレクトリ自体の操作には`File`クラスを使用する。
  • リソースの解放漏れを防ぐために`try-with-resources`文を積極的に活用する。

java.ioはJavaにおけるI/O処理の基礎です。ここで学んだ知識は、より高度なjava.nioを学ぶ上でも必ず役立ちます。ぜひ、実際にコードを書きながら、ストリームの操作に慣れていってください。

モバイルバージョンを終了