Java I/O ストリーム架構:ノードストリームと処理ストリームの活用

ストリームの分類体系

Java における I/O ストリームは、複数の観点から分類することができます。まず、データの流向に基づき、入力ストリームと出力ストリームに大別されます。次に、処理されるデータの単位によって、バイトストリームと文字ストリームに分けられます。バイトストリームは 8 ビットの字节を最小単位とし、文字ストリームは Unicode 字符(通常 2 バイト)を扱います。

機能上の役割による分類では、ノードストリームと処理ストリームという概念が重要です。ノードストリームは、ファイルやメモリなどの具体的なデータソースに直接接続して読み書きを行うクラスを指します。一方、処理ストリームは既存のストリームをラップし、 buffering や変換などの追加機能を提供する间接的なクラスです。

ノードストリームの概要

ノードストリームは、特定のデバイスやデータ源に直接アクセスするための基盤となります。主な種類は以下の通りです。

  • ファイルストリーム: ディスク上のファイルに対して読み書きを行います。代表的なクラスには FileInputStreamFileOutputStreamFileReaderFileWriter があります。
  • メモリストリーム: 配列や文字列といった内存上のデータ領域を対象とします。ByteArrayInputStreamCharArrayReader などが該当し、テンポラリなデータ保持に利用されます。
  • パイプストリーム: スレッド間やプロセス間の通信を実現します。PipedInputStreamPipedOutputStream を組み合わせることで、データの受け渡しが可能になります。

処理ストリームの種類と役割

処理ストリームは、ノードストリームを装飾することで機能を拡張します。主要なカテゴリは以下の通りです。

  • バッファリングストリーム: 内部に缓冲区を設けることで I/O 回数を減らし、性能を向上させます。BufferedReaderBufferedInputStream など。
  • 変換ストリーム: バイト列と文字列の相互変換を行います。文字コードの指定が可能で、InputStreamReaderOutputStreamWriter が含まれます。
  • データストリーム: Java のプリミティブ型データをバイナリ形式で読み書きします。DataInputStreamDataOutputStream が該当します。
  • その他: オブジェクト直列化用のオブジェクトストリーム、行番号を管理するカウンティングストリーム、印刷用のプリントストリームなど多様な機能があります。

バッファリングストリームの詳細

バッファリングは I/O 性能最適化の基本的な手法です。缓冲区を導入することで、一度に複数のデータ単位を処理できるようになり、システム全体のスループットが向上します。また、markreset といった位置制御メソッドの利用も可能になります。

処理ストリームは、対応するノードストリームをコンストラクタ引数として受け取る形で「接続」されます。これにより、readLinenewLine といった高レベルなメソッドが利用可能になります。出力時には、データが缓冲区に溜められるため、明示的に flush メソッドを呼び出してデータを強制書き込みする必要があります。

以下の例では、バッファ付き入力ストリームを用いて、読み込み位置のマークとリセット機能を実演します。

import java.io.*;

public class BufferedStreamDemo {
    public static void main(String[] args) {
        // リソース自動管理を使用
        try (FileInputStream inputStream = new FileInputStream("config.txt");
             BufferedInputStream bufferedStream = new BufferedInputStream(inputStream)) {
            
            int data = 0;
            // 最初の 1 文字を読み込み
            System.out.println("First char: " + (char) bufferedStream.read());
            
            // 当前位置にマークを設定(最大 200 バイトまで戻り可能)
            bufferedStream.mark(200);
            
            // マーク以降のデータを数文字読み込み
            for (int i = 0; i < 5 && (data = bufferedStream.read()) != -1; i++) {
                System.out.print((char) data + " ");
            }
            System.out.println();
            
            // マーク位置までポインタを戻す
            bufferedStream.reset();
            
            // 再び同じデータを読み込み
            for (int i = 0; i < 5 && (data = bufferedStream.read()) != -1; i++) {
                System.out.print((char) data + " ");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

次に、バッファ付き出力ストリームを用いて、テキストファイルへの書き込み処理を行います。

import java.io.*;

public class BufferedWriteDemo {
    public static void main(String[] args) {
        try (FileWriter fileWriter = new FileWriter("output_log.txt");
             BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
             FileReader fileReader = new FileReader("output_log.txt");
             BufferedReader bufferedReader = new BufferedReader(fileReader)) {
            
            // タイムスタンプを 10 行書き込み
            for (int i = 0; i < 10; i++) {
                String timestamp = String.valueOf(System.currentTimeMillis());
                bufferedWriter.write(timestamp);
                bufferedWriter.newLine();
            }
            // 缓冲区の内容をファイルに強制出力
            bufferedWriter.flush();
            
            // 書き込んだ内容を読み込んで表示
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

変換ストリームの活用

変換ストリームは、バイトストリームと文字ストリームの橋渡し 역할을 합니다。キーボード入力(System.in)はバイトストリームですが、テキスト処理には文字ストリームが適しているため、InputStreamReader を介して変換します。ストリーム選定の際は、データ源、データ種別(テキストかバイナリか)、そして必要な付加機能(バッファなど)を順に考慮します。

以下のコードは、コンソールからの入力をファイルに保存し、特定の終了コマンドで処理を終わらせる例です。

import java.io.*;

public class ConverterStreamDemo {
    public static void main(String[] args) throws IOException {
        // キーボード入力(バイト)を文字ストリームに変換し、さらにバッファリング
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
             BufferedWriter writer = new BufferedWriter(new FileWriter("input_record.txt"))) {
            
            String inputLine;
            while ((inputLine = reader.readLine()) != null) {
                // "quit" と入力されたらループを抜ける
                if ("quit".equalsIgnoreCase(inputLine)) {
                    break;
                }
                writer.write(inputLine);
                writer.newLine();
            }
        }
    }
}

データストリームによる型維持

DataInputStreamDataOutputStream は、Java の基本データ型をプラットフォームに依存しない形式で読み書きするために使用されます。これらは処理ストリームであるため、基底となるバイトストリームに接続して使用します。データを書き込んだ順序と読み込む順序を厳密に一致させる必要があります。

以下の例では、整数、文字列、浮動小数点数をファイルに保存し、再び読み込んで復元します。

import java.io.*;

public class DataStreamDemo {
    public static void main(String[] args) throws IOException {
        String filePath = "data_store.bin";
        
        // データの書き込み
        try (FileOutputStream fos = new FileOutputStream(filePath);
             BufferedOutputStream bos = new BufferedOutputStream(fos);
             DataOutputStream dos = new DataOutputStream(bos)) {
            
            dos.writeBoolean(true);
            dos.writeInt(2023);
            dos.writeDouble(98.5);
            // 最上位のストリームを閉じると、下層のストリームも自動的に閉じられる
        }
        
        // データの読み込み
        try (FileInputStream fis = new FileInputStream(filePath);
             BufferedInputStream bis = new BufferedInputStream(fis);
             DataInputStream dis = new DataInputStream(bis)) {
            
            boolean flag = dis.readBoolean();
            int year = dis.readInt();
            double score = dis.readDouble();
            
            System.out.println("Boolean: " + flag);
            System.out.println("Year: " + year);
            System.out.println("Score: " + score);
        }
    }
}

タグ: Java InputStream OutputStream BufferedReader InputStreamReader

6月15日 17:12 投稿