Javaファイル入出力完全ガイド

2025-08-02

はじめに

Javaプログラミングにおいて、ファイルの読み書きはデータ永続化や外部システムとの連携に欠かせないスキルです。この記事では、Javaのファイル入出力(I/O)について、基礎から実践的なテクニックまでを網羅的に解説します。すでにJavaの基本文法や例外処理を理解していることを前提に、ファイル操作に特化した内容をお届けします。

ファイル入出力を学ぶことで、データを永続的に保存できるようになり、外部システムとのデータ交換も可能になります。また、設定ファイルやログファイルを扱うスキルを身につけられるため、システム管理や開発の幅が広がります。さらに、大規模なデータを効率的に処理できるようになり、実践的なプログラミング力の向上にもつながります。

Java I/Oの基本概念

ストリームとは?

JavaのI/O操作は「ストリーム」という概念を中心に設計されています。ストリームとは、データの流れを抽象化したもので、以下の2種類があります。

バイトストリーム

データを1バイト単位で読み書きする入出力処理の仕組みです。主に画像・音声・動画などのバイナリデータを扱う際に使用され、InputStreamOutputStream クラスを基本として動作します。

文字ストリーム

文字データを2バイト(Unicode)単位で読み書きする入出力処理の仕組みです。テキストファイルなどの文字データを扱うときに使用され、ReaderWriter クラスを基本として動作します。

主要なI/Oクラス階層

InputStream (バイト入力)
├── FileInputStream
├── BufferedInputStream
└── ObjectInputStream

OutputStream (バイト出力)
├── FileOutputStream
├── BufferedOutputStream
└── ObjectOutputStream

Reader (文字入力)
├── InputStreamReader
├── FileReader
└── BufferedReader

Writer (文字出力)
├── OutputStreamWriter
├── FileWriter
└── BufferedWriter

テキストファイルの読み書き

ファイル読み込みの基本

Javaのファイル読み込みは、指定したファイルを開いてデータをプログラムに取り込み、行単位またはバイト単位で内容を処理する仕組みであり、主にInputStreamReaderを利用して行われます。

方法1: FileReader + BufferedReader (従来型)

このコードは、BufferedReaderFileReader を使って "example.txt" の内容を1行ずつ読み取り、すべての行をコンソールに出力するプログラムです。

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TextFileReader {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("example.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("ファイル読み込みエラー: " + e.getMessage());
        }
    }
}

方法2: Files.readAllLines (Java 7以降)

このコードは、Files.readAllLines() を使って "example.txt" の全行を一度に読み込み、各行を順にコンソールへ出力するプログラムです。

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class ModernTextFileReader {
    public static void main(String[] args) {
        try {
            List lines = Files.readAllLines(Paths.get("example.txt"));
            lines.forEach(System.out::println);
        } catch (IOException e) {
            System.err.println("ファイル読み込みエラー: " + e.getMessage());
        }
    }
}

方法3: Files.lines (Java 8以降, ストリームAPI利用)

このコードは、Files.lines() を使って "example.txt" の内容をストリームとして読み込み、各行を順にコンソールへ出力するプログラムです。

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;

public class StreamFileReader {
    public static void main(String[] args) {
        try (Stream lines = Files.lines(Paths.get("example.txt"))) {
            lines.forEach(System.out::println);
        } catch (IOException e) {
            System.err.println("ファイル読み込みエラー: " + e.getMessage());
        }
    }
}

ファイル書き込みの基本

Javaのファイル書き込みは、指定したファイルを開いてデータをプログラムから出力し、文字やバイト単位で内容を保存する仕組みであり、主にOutputStreamWriterを利用して行われます。

方法1: FileWriter + BufferedWriter (従来型)

このコードは、BufferedWriterFileWriter を使って "output.txt" に文字列を書き込み、改行を挟んで複数行のテキストを出力するプログラムです。

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class TextFileWriter {
    public static void main(String[] args) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
            writer.write("Hello, Java IO!");
            writer.newLine();
            writer.write("2行目です");
        } catch (IOException e) {
            System.err.println("ファイル書き込みエラー: " + e.getMessage());
        }
    }
}

方法2: Files.write (Java 7以降)

このコードは、Files.write() メソッドを使って "output.txt" に複数行の文字列リストを一度に書き込むプログラムです。

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class ModernTextFileWriter {
    public static void main(String[] args) {
        List lines = Arrays.asList("1行目", "2行目", "3行目");
        try {
            Files.write(Paths.get("output.txt"), lines);
        } catch (IOException e) {
            System.err.println("ファイル書き込みエラー: " + e.getMessage());
        }
    }
}

バイナリファイルの読み書き

バイナリファイルの読み書きは、画像や音声などの非テキストデータを1バイト単位で扱う処理であり、Javaでは主にInputStreamOutputStreamを使用して実現します。

バイナリファイル読み込み

このコードは、FileInputStream を使って "image.jpg" を1KBずつ読み込み、読み取ったバイナリデータを processData メソッドで処理するプログラムです。

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

public class BinaryFileReader {
    public static void main(String[] args) {
        try (FileInputStream fis = new FileInputStream("image.jpg"))) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                // バッファ内のデータを処理(bytesReadは実際に読み込んだバイト数)
                processData(buffer, bytesRead);
            }
        } catch (IOException e) {
            System.err.println("ファイル読み込みエラー: " + e.getMessage());
        }
    }

    private static void processData(byte[] data, int length) {
        // バイナリデータ処理の例
        System.out.println(length + "バイト処理しました");
    }
}

バイナリファイル書き込み

このコードは、FileOutputStream を使って "binary.bin""Hello" の文字列を表すバイト配列を書き込むプログラムです。

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

public class BinaryFileWriter {
    public static void main(String[] args) {
        byte[] data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello"のASCIIコード
        try (FileOutputStream fos = new FileOutputStream("binary.bin"))) {
            fos.write(data);
        } catch (IOException e) {
            System.err.println("ファイル書き込みエラー: " + e.getMessage());
        }
    }
}

ファイルとディレクトリ操作

Javaのファイルとディレクトリ操作は、FileFiles クラスを利用して、ファイルやフォルダの作成・削除・移動・コピー・存在確認などを行う仕組みです。

ファイル・ディレクトリ情報の取得

このコードは、PathsFiles クラスを使って "example.txt" の名前・パス・サイズ・最終更新日時・アクセス権などの情報を取得して表示するプログラムです。

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;

public class FileInfo {
    public static void main(String[] args) {
        Path path = Paths.get("example.txt");

        System.out.println("ファイル名: " + path.getFileName());
        System.out.println("親ディレクトリ: " + path.getParent());
        System.out.println("絶対パス: " + path.toAbsolutePath());

        try {
            System.out.println("サイズ: " + Files.size(path) + "バイト");
            System.out.println("最終更新日時: " + Files.getLastModifiedTime(path));
            System.out.println("読み取り可能? " + Files.isReadable(path));
            System.out.println("通常ファイル? " + Files.isRegularFile(path));
        } catch (IOException e) {
            System.err.println("エラー: " + e.getMessage());
        }
    }
}

ファイル・ディレクトリの作成・削除

このコードは、FilesPaths クラスを使って新しいディレクトリを作成し、その中にファイルを生成してから削除し、最後に空になったディレクトリ自体も削除する一連の操作を行うプログラムです。

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;

public class FileDirectoryOperations {
    public static void main(String[] args) {
        // ディレクトリ作成
        Path dir = Paths.get("new_directory");
        try {
            if (!Files.exists(dir)) {
                Files.createDirectory(dir);
                System.out.println("ディレクトリ作成: " + dir);
            }

            // ファイル作成
            Path file = dir.resolve("test.txt"); // ディレクトリ内にパスを解決
            if (!Files.exists(file)) {
                Files.createFile(file);
                System.out.println("ファイル作成: " + file);
            }

            // 削除
            Files.deleteIfExists(file);
            System.out.println("ファイル削除: " + file);

            // ディレクトリ削除(空でないと削除できない)
            Files.deleteIfExists(dir);
            System.out.println("ディレクトリ削除: " + dir);

        } catch (IOException e) {
            System.err.println("操作エラー: " + e.getMessage());
        }
    }
}

高度なファイル操作

ファイルのコピーと移動

このコードは、Files.copy()Files.move() を使って "source.txt" をコピーし、続いて同じファイルを別名 "moved_source.txt" に移動(またはリネーム)するプログラムです。

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.nio.file.StandardCopyOption;

public class FileCopyMove {
    public static void main(String[] args) {
        Path source = Paths.get("source.txt");
        Path targetCopy = Paths.get("copy_of_source.txt");
        Path targetMove = Paths.get("moved_source.txt");

        try {
            // ファイルコピー(既存ファイルは上書き)
            Files.copy(source, targetCopy, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("ファイルコピー完了");

            // ファイル移動(リネーム)
            Files.move(source, targetMove, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("ファイル移動完了");

        } catch (IOException e) {
            System.err.println("操作エラー: " + e.getMessage());
        }
    }
}

ディレクトリ走査

このコードは、Files.walk() を使って現在のディレクトリ以下を再帰的に走査し、さらに Files.list() で直下のファイルとディレクトリのみを一覧表示するプログラムです。

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;

public class DirectoryWalker {
    public static void main(String[] args) {
        Path startDir = Paths.get(".");

        // 方法1: Files.walk (再帰的)
        try (Stream paths = Files.walk(startDir)) {
            System.out.println("--- すべてのファイルとディレクトリ ---");
            paths.forEach(System.out::println);
        } catch (IOException e) {
            System.err.println("走査エラー: " + e.getMessage());
        }

        // 方法2: Files.list (非再帰的)
        try (Stream paths = Files.list(startDir)) {
            System.out.println("--- 直下のファイルとディレクトリ ---");
            paths.forEach(System.out::println);
        } catch (IOException e) {
            System.err.println("走査エラー: " + e.getMessage());
        }
    }
}

オブジェクトのシリアライズ

Javaオブジェクトをファイルに保存・読み込みする方法です。

オブジェクトの書き込み

このコードは、ObjectOutputStream を使って Person オブジェクトのリストをシリアライズし、people.dat ファイルに書き込むことでオブジェクトデータを保存するプログラムです。

import java.io.*;
import java.util.ArrayList;
import java.util.List;

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

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class ObjectSerialization {
    public static void main(String[] args) {
        List people = new ArrayList<>();
        people.add(new Person("山田太郎", 25));
        people.add(new Person("佐藤花子", 30));

        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("people.dat"))) {
            oos.writeObject(people);
            System.out.println("オブジェクト書き込み完了");
        } catch (IOException e) {
            System.err.println("シリアライズエラー: " + e.getMessage());
        }
    }
}

オブジェクトの読み込み

このコードは、ObjectInputStream を使って people.dat に保存された Person オブジェクトのリストをデシリアライズし、読み込んだ内容をコンソールに表示するプログラムです。

import java.io.*;
import java.util.List;

public class ObjectDeserialization {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("people.dat"))) {
            List people = (List) ois.readObject();
            System.out.println("読み込んだオブジェクト:");
            people.forEach(System.out::println);
        } catch (IOException | ClassNotFoundException e) {
            System.err.println("デシリアライズエラー: " + e.getMessage());
        }
    }
}

実践的なファイル処理例

ログファイルの解析

このコードは、Files.lines() を使って "server.log" の内容を読み込み、StreamCollectors を利用して「ERROR」を含む行を日付ごとに集計し、エラー発生件数を日付別に出力するログ解析プログラムです。

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class LogAnalyzer {
    public static void main(String[] args) {
        String logFile = "server.log";

        try (Stream lines = Files.lines(Paths.get(logFile))) {
            // エラーレベルのログをカウント
            Map errorCounts = lines
                .filter(line -> line.contains("ERROR"))
                .map(line -> line.split(" ")[0]) // 日付部分を取得
                .collect(Collectors.groupingBy(
                    date -> date,
                    Collectors.counting()
                ));

            System.out.println("エラー発生状況:");
            errorCounts.forEach((date, count) -> 
                System.out.println(date + ": " + count + "件"));

        } catch (IOException e) {
            System.err.println("ログ解析エラー: " + e.getMessage());
        }
    }
}

設定ファイルの読み込み

この設定ファイルは、データベース接続情報(URL・ユーザー名・パスワード)やアプリケーションのタイムアウト時間など、プログラムの動作設定を外部から指定するためのプロパティファイルです。

# config.properties
database.url=jdbc:mysql://localhost:3306/mydb
database.user=admin
database.password=secret
app.timeout=5000

このコードは、Properties クラスを使って config.properties ファイルを読み込み、データベース接続情報やアプリのタイムアウト設定を取得して表示するプログラムです。

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class ConfigLoader {
    public static void main(String[] args) {
        Properties props = new Properties();

        try (FileInputStream fis = new FileInputStream("config.properties")) {
            props.load(fis);

            String dbUrl = props.getProperty("database.url");
            String dbUser = props.getProperty("database.user");
            String dbPassword = props.getProperty("database.password");
            int timeout = Integer.parseInt(props.getProperty("app.timeout"));

            System.out.println("DB接続情報:");
            System.out.println("URL: " + dbUrl);
            System.out.println("ユーザー: " + dbUser);
            System.out.println("タイムアウト: " + timeout + "ms");

        } catch (IOException e) {
            System.err.println("設定ファイル読み込みエラー: " + e.getMessage());
        }
    }
}

ファイル入出力のベストプラクティス

リソース管理

ファイル入出力では、try-with-resources構文を使ってストリームを自動的に閉じることで、使用後のリソースを確実に解放し、メモリリークやファイルロックを防ぐことが重要です。

エンコーディングの明示

ファイル読み込み時に文字エンコーディングを明示的に指定する重要性を示しており、FileReader のようにデフォルトエンコーディングに依存する方法は避け、InputStreamReaderStandardCharsets.UTF_8 を使って明確に指定するのが望ましいことを示しています。

// 悪い例(プラットフォーム依存のデフォルトエンコーディング)
new FileReader("file.txt");

// 良い例(明示的なエンコーディング指定)
new InputStreamReader(new FileInputStream("file.txt"), StandardCharsets.UTF_8);

バッファリングの活用

ファイルを1文字ずつ読み込む非効率な方法と、BufferedReader を使って行単位で処理する効率的な方法を比較しており、大きなファイルを扱う際はバッファ付きストリームを利用して処理速度を向上させることが推奨されることを示しています。

// 非効率的
try (FileReader fr = new FileReader("large.txt")) {
    int c;
    while ((c = fr.read()) != -1) { // 1文字ずつ読み込み
        // 処理
    }
}

// 効率的
try (BufferedReader br = new BufferedReader(new FileReader("large.txt"))) {
    String line;
    while ((line = br.readLine()) != null) { // 行単位で読み込み
        // 処理
    }
}

大規模ファイル処理

Files.readAllLines() がファイル全体をメモリに読み込むため大規模ファイルには不向きである一方、Files.lines() はストリームを使って行ごとに処理するため、メモリ効率が良く大きなファイルにも適していることを示しています。

// メモリ効率の悪い方法(小~中規模ファイル向け)
List lines = Files.readAllLines(Paths.get("huge_file.txt"));

// メモリ効率の良い方法(大規模ファイル向け)
try (Stream lines = Files.lines(Paths.get("huge_file.txt"))) {
    lines.forEach(line -> {
        // 行ごとの処理
    });
}

一時ファイルの安全な使用

Files.createTempFile() を使って一時ファイルを作成し、deleteOnExit() によりプログラム終了時に自動削除されるよう設定することで、作業用ファイルを安全かつ一時的に扱う方法を示しています。

import java.nio.file.Files;
import java.nio.file.Path;

public class TempFileExample {
    public static void main(String[] args) {
        try {
            // 一時ファイル作成
            Path tempFile = Files.createTempFile("prefix_", ".suffix");
            System.out.println("一時ファイル: " + tempFile);

            // 使用後削除(JVM終了時にも自動削除)
            tempFile.toFile().deleteOnExit();

            // 一時ファイルを使用...

        } catch (IOException e) {
            System.err.println("一時ファイル作成エラー: " + e.getMessage());
        }
    }
}

よくあるエラーと対処法

FileNotFoundException

FileNotFoundException は、指定したファイルが存在しない、またはアクセスできない場合に発生する例外です。たとえば、指定したパスにファイルが存在しないときや、読み取り・書き込みの権限がないときにスローされます。

Path path = Paths.get("file.txt");
if (!Files.exists(path)) {
    System.err.println("ファイルが見つかりません: " + path);
    return;
}

AccessDeniedException

AccessDeniedException は、ファイルやディレクトリに対してアクセス権限がない場合に発生する例外です。たとえば、読み取り専用ファイルへの書き込みや、権限のないディレクトリへの操作を行おうとしたときにスローされます。

文字化け

文字化けは、ファイルのエンコーディング(文字コード)の不一致が原因で発生します。たとえば、UTF-8で保存されたファイルをShift_JISとして読み込むと、文字が正しく解釈されずに意味不明な記号や文字が表示されます。

これを防ぐには、エンコーディングを明示的に指定してファイルを読み書きすることが重要です。具体的には、次のように InputStreamReaderOutputStreamWriterStandardCharsets.UTF_8 を指定することで、文字コードのずれを防ぐことができます。

Files.readAllLines(path, StandardCharsets.UTF_8);

OutOfMemoryError

OutOfMemoryError は、巨大なファイルを一度にメモリへ読み込もうとした場合などに発生するエラーです。ファイル全体をまとめて処理すると、ヒープ領域を使い果たしてプログラムが強制終了することがあります。

これを防ぐには、ストリーム処理やバッファリングを用いて、データを少しずつ分割して読み込む方法が有効です。例えば、BufferedReaderFiles.lines() を使えば、行ごとに処理できるためメモリ使用量を大幅に抑えられます。

まとめ

この記事では、Javaにおけるファイル入出力の仕組みを包括的に解説し、テキストファイル処理ではReader/WriterクラスやFilesユーティリティの活用法、バイナリファイル処理ではInputStream/OutputStreamクラスの使用法、さらにファイルシステム操作によるファイルやディレクトリの作成・削除・コピー、オブジェクトシリアライズによるデータの永続化について説明しました。また、リソース管理やエンコーディング指定、バッファリングといったベストプラクティスも紹介しました。

ファイル操作はJavaプログラミングの基本スキルです。安全で効率的なコードを書くために、適切な例外処理とリソース管理を常に心がけましょう。最初はシンプルなファイル操作から始め、徐々に複雑な処理に挑戦していくのがおすすめです。

演習問題

初級問題 (3問)

問題1: 基本的なファイル読み書き

FileInputStreamとFileOutputStreamを使用して、バイナリファイルのコピーを行うプログラムを作成してください。512バイトのバッファを使用し、FileNotFoundExceptionを適切に処理するようにしてください。

public class FileCopyBasic {
    public static void copyFile(String sourcePath, String destinationPath) {
        // ここに実装
    }
}

問題2: テキストファイルの読み込みと処理

BufferedReaderを使用してテキストファイルを読み込み、行ごとに処理するプログラムを作成してください。ファイルが存在しない場合はFileNotFoundExceptionを、アクセス権限がない場合はAccessDeniedExceptionをキャッチして適切なエラーメッセージを表示してください。

public class TextFileReader {
    public static void readAndProcessFile(String filePath) {
        // ここに実装
    }
}

問題3: プロパティファイルの書き込み

PropertiesクラスとFileOutputStreamを使用して、設定情報をプロパティファイルに保存するプログラムを作成してください。BufferedOutputStreamでラップして効率化してください。

public class PropertiesWriter {
    public static void saveProperties(String filePath, Properties properties) {
        // ここに実装
    }
}

中級問題 (6問)

問題4: 大容量ファイルのストリーミング処理

大きなファイルを処理する際にOutOfMemoryErrorが発生しないように、BufferedInputStreamを使用してチャンクごとに処理するプログラムを作成してください。ファイルサイズに応じてバッファサイズを動的に調整する機能を追加してください。

public class LargeFileProcessor {
    public static void processLargeFile(String filePath, int chunkSize) {
        // ここに実装
    }
}

問題5: オブジェクトのシリアライズとデシリアライズ

ObjectInputStreamとObjectOutputStreamを使用して、Javaオブジェクトをファイルに保存し、読み込むプログラムを作成してください。Serializableインターフェースを実装したサンプルクラスも作成してください。

public class User implements Serializable {
    // 実装
}

public class ObjectSerializer {
    public static void saveObject(String filePath, Object obj) {
        // ここに実装
    }

    public static Object loadObject(String filePath) {
        // ここに実装
    }
}

問題6: 文字エンコーディングの変換

InputStreamReaderとOutputStreamWriterを使用して、ファイルの文字エンコーディングを変換するプログラムを作成してください。Shift-JISからUTF-8への変換を例に実装してください。

public class EncodingConverter {
    public static void convertEncoding(String sourcePath, String destPath, 
                                     String sourceEncoding, String destEncoding) {
        // ここに実装
    }
}

問題7: ログファイルの追記処理

FileWriterを使用してログファイルに追記するプログラムを作成してください。BufferedWriterでラップし、タイムスタンプとログレベルを追加する機能を実装してください。ファイルが存在しない場合は新規作成、存在する場合は追記モードで開くようにしてください。

public class Logger {
    public static void log(String filePath, String level, String message) {
        // ここに実装
    }
}

問題8: 複数ファイルの結合処理

複数のテキストファイルを1つのファイルに結合するプログラムを作成してください。FileReaderとFileWriterを使用し、各ファイルの内容の間に区切り線を追加する機能を実装してください。

public class FileMerger {
    public static void mergeFiles(String[] sourcePaths, String destPath) {
        // ここに実装
    }
}

問題9: バイナリファイルの部分読み込み

RandomAccessFileを使用して、大きなバイナリファイルの特定の位置からデータを読み込むプログラムを作成してください。指定された範囲のバイトデータを抽出する機能を実装してください。

public class BinaryFileReader {
    public static byte[] readFileSegment(String filePath, long start, int length) {
        // ここに実装
    }
}

上級問題 (3問)

問題10: メモリマップトファイルの使用

MappedByteBufferを使用して、メモリマップトファイルによる高速なファイル操作を実装してください。大きなファイルをメモリにマップし、効率的な読み書きを行うプログラムを作成してください。

public class MemoryMappedFileHandler {
    public static void processWithMemoryMapping(String filePath, long fileSize) {
        // ここに実装
    }
}

問題11: 非同期ファイル入出力

AsynchronousFileChannelを使用して、非同期ファイル入出力を実装してください。コールバック関数を利用した非同期処理と、Futureを利用した処理の両方を実装してください。

public class AsyncFileProcessor {
    public static void readAsyncWithCallback(String filePath) {
        // ここに実装
    }

    public static Future readAsyncWithFuture(String filePath) {
        // ここに実装
    }
}

問題12: カスタムファイルシステムのシミュレーション

仮想のファイルシステムをシミュレートするプログラムを作成してください。メモリ上にファイルシステムを構築し、ディレクトリ構造、ファイルの作成・読み書き・削除機能を実装してください。独自のInputStream/OutputStreamを実装して、メモリ上のデータを操作できるようにしてください。

public class VirtualFileSystem {
    // 仮想ファイルシステムの実装
}

public class VirtualFileInputStream extends InputStream {
    // カスタムInputStreamの実装
}

public class VirtualFileOutputStream extends OutputStream {
    // カスタムOutputStreamの実装
}

演習問題 解答例

初級問題 解答例

問題1: 基本的なファイル読み書き

import java.io.*;

public class FileCopyBasic {
    public static void copyFile(String sourcePath, String destinationPath) {
        try (FileInputStream fis = new FileInputStream(sourcePath);
             FileOutputStream fos = new FileOutputStream(destinationPath)) {

            byte[] buffer = new byte[512];
            int bytesRead;

            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }

            System.out.println("ファイルのコピーが完了しました");

        } catch (FileNotFoundException e) {
            System.err.println("ファイルが見つかりません: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("入出力エラーが発生しました: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        copyFile("source.txt", "destination.txt");
    }
}

問題2: テキストファイルの読み込みと処理

import java.io.*;
import.nio.file.AccessDeniedException;

public class TextFileReader {
    public static void readAndProcessFile(String filePath) {
        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String line;
            int lineNumber = 1;

            while ((line = reader.readLine()) != null) {
                System.out.println("Line " + lineNumber + ": " + line);
                lineNumber++;
            }

        } catch (FileNotFoundException e) {
            System.err.println("ファイルが見つかりません: " + filePath);
        } catch (AccessDeniedException e) {
            System.err.println("ファイルへのアクセスが拒否されました: " + filePath);
        } catch (IOException e) {
            System.err.println("入出力エラーが発生しました: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        readAndProcessFile("sample.txt");
    }
}

問題3: プロパティファイルの書き込み

import java.io.*;
import java.util.Properties;

public class PropertiesWriter {
    public static void saveProperties(String filePath, Properties properties) {
        try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) {

            properties.store(bos, "Application Configuration");
            System.out.println("プロパティファイルを保存しました: " + filePath);

        } catch (IOException e) {
            System.err.println("プロパティの保存中にエラーが発生しました: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        Properties props = new Properties();
        props.setProperty("database.url", "jdbc:mysql://localhost:3306/mydb");
        props.setProperty("database.username", "admin");
        props.setProperty("database.password", "secret");

        saveProperties("config.properties", props);
    }
}

中級問題 解答例

問題4: 大容量ファイルのストリーミング処理

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;

public class LargeFileProcessor {
    public static void processLargeFile(String filePath, int chunkSize) {
        try {
            long fileSize = Files.size(Paths.get(filePath));
            int bufferSize = calculateOptimalBufferSize(fileSize, chunkSize);

            System.out.println("ファイルサイズ: " + fileSize + " bytes");
            System.out.println("バッファサイズ: " + bufferSize + " bytes");

            try (BufferedInputStream bis = new BufferedInputStream(
                     new FileInputStream(filePath), bufferSize)) {

                byte[] buffer = new byte[chunkSize];
                int bytesRead;
                long totalBytes = 0;

                while ((bytesRead = bis.read(buffer)) != -1) {
                    processChunk(buffer, bytesRead);
                    totalBytes += bytesRead;

                    System.out.printf("処理進捗: %.2f%%\n", 
                        (totalBytes * 100.0) / fileSize);
                }

            }
        } catch (OutOfMemoryError e) {
            System.err.println("メモリ不足エラー: バッファサイズを小さくしてください");
        } catch (IOException e) {
            System.err.println("ファイル処理中にエラーが発生: " + e.getMessage());
        }
    }

    private static int calculateOptimalBufferSize(long fileSize, int chunkSize) {
        int maxBuffer = 1024 * 1024; // 1MB
        if (fileSize > 100 * 1024 * 1024) { // 100MB以上
            return Math.min(chunkSize, maxBuffer);
        }
        return Math.min(chunkSize, 8192); // 小さいファイルは8KB
    }

    private static void processChunk(byte[] chunk, int length) {
        // チャンクデータの処理をシミュレート
        System.out.println("チャンク処理: " + length + " bytes");
    }

    public static void main(String[] args) {
        processLargeFile("largefile.dat", 8192);
    }
}

問題5: オブジェクトのシリアライズとデシリアライズ

import java.io.*;
import java.util.Date;

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

    private String name;
    private String email;
    private Date birthDate;
    private transient String password; // シリアライズしない

    public User(String name, String email, Date birthDate, String password) {
        this.name = name;
        this.email = email;
        this.birthDate = birthDate;
        this.password = password;
    }

    // ゲッター・セッター
    @Override
    public String toString() {
        return "User{name='" + name + "', email='" + email + 
               "', birthDate=" + birthDate + "}";
    }
}

public class ObjectSerializer {
    public static void saveObject(String filePath, Object obj) {
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new BufferedOutputStream(new FileOutputStream(filePath)))) {

            oos.writeObject(obj);
            System.out.println("オブジェクトを保存しました: " + filePath);

        } catch (IOException e) {
            System.err.println("オブジェクトの保存中にエラー: " + e.getMessage());
        }
    }

    public static Object loadObject(String filePath) {
        try (ObjectInputStream ois = new ObjectInputStream(
                new BufferedInputStream(new FileInputStream(filePath)))) {

            Object obj = ois.readObject();
            System.out.println("オブジェクトを読み込みました: " + filePath);
            return obj;

        } catch (IOException | ClassNotFoundException e) {
            System.err.println("オブジェクトの読み込み中にエラー: " + e.getMessage());
            return null;
        }
    }

    public static void main(String[] args) {
        User user = new User("山田太郎", "taro@example.com", new Date(), "secret123");

        // 保存
        saveObject("user.dat", user);

        // 読み込み
        User loadedUser = (User) loadObject("user.dat");
        System.out.println("読み込んだユーザー: " + loadedUser);
    }
}

問題6: 文字エンコーディングの変換

import java.io.*;

public class EncodingConverter {
    public static void convertEncoding(String sourcePath, String destPath, 
                                     String sourceEncoding, String destEncoding) {
        try (InputStreamReader isr = new InputStreamReader(
                 new FileInputStream(sourcePath), sourceEncoding);
             OutputStreamWriter osw = new OutputStreamWriter(
                 new FileOutputStream(destPath), destEncoding);
             BufferedReader reader = new BufferedReader(isr);
             BufferedWriter writer = new BufferedWriter(osw)) {

            char[] buffer = new char[8192];
            int charsRead;

            while ((charsRead = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, charsRead);
            }

            System.out.println("エンコーディング変換完了: " + sourceEncoding + " → " + destEncoding);

        } catch (UnsupportedEncodingException e) {
            System.err.println("サポートされていないエンコーディング: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("変換中にエラーが発生: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        convertEncoding("shift_jis.txt", "utf8.txt", "Shift_JIS", "UTF-8");
    }
}

問題7: ログファイルの追記処理

import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Logger {
    private static final DateTimeFormatter TIMESTAMP_FORMAT = 
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void log(String filePath, String level, String message) {
        try (BufferedWriter writer = new BufferedWriter(
                new FileWriter(filePath, true))) { // true = append mode

            String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
            String logEntry = String.format("[%s] [%s] %s", timestamp, level, message);

            writer.write(logEntry);
            writer.newLine();
            writer.flush();

            System.out.println("ログを記録しました: " + logEntry);

        } catch (IOException e) {
            System.err.println("ログの書き込み中にエラー: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        log("application.log", "INFO", "アプリケーションが起動しました");
        log("application.log", "WARN", "設定ファイルが見つかりません");
        log("application.log", "ERROR", "データベース接続に失敗しました");
    }
}

問題8: 複数ファイルの結合処理

import java.io.*;

public class FileMerger {
    public static void mergeFiles(String[] sourcePaths, String destPath) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(destPath))) {

            for (int i = 0; i < sourcePaths.length; i++) {
                try (BufferedReader reader = new BufferedReader(
                         new FileReader(sourcePaths[i]))) {

                    // ファイル区切り線を追加(最初のファイル以外)
                    if (i > 0) {
                        writer.write("--- " + sourcePaths[i] + " ---");
                        writer.newLine();
                    }

                    String line;
                    while ((line = reader.readLine()) != null) {
                        writer.write(line);
                        writer.newLine();
                    }

                    System.out.println("ファイルを結合: " + sourcePaths[i]);

                } catch (FileNotFoundException e) {
                    System.err.println("ファイルが見つかりません: " + sourcePaths[i]);
                }
            }

            System.out.println("ファイルの結合が完了しました: " + destPath);

        } catch (IOException e) {
            System.err.println("ファイル結合中にエラー: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        String[] files = {"file1.txt", "file2.txt", "file3.txt"};
        mergeFiles(files, "merged.txt");
    }
}

問題9: バイナリファイルの部分読み込み

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;

public class BinaryFileReader {
    public static byte[] readFileSegment(String filePath, long start, int length) {
        try (RandomAccessFile raf = new RandomAccessFile(filePath, "r")) {

            long fileSize = raf.length();

            // 範囲チェック
            if (start < 0 || start >= fileSize) {
                throw new IllegalArgumentException("開始位置がファイル範囲外です");
            }

            if (start + length > fileSize) {
                length = (int) (fileSize - start);
                System.out.println("読み込み長さを調整: " + length);
            }

            raf.seek(start);
            byte[] buffer = new byte[length];
            int bytesRead = raf.read(buffer);

            if (bytesRead != length) {
                System.out.println("要求された長さより少なく読み込み: " + bytesRead + " bytes");
            }

            System.out.println("ファイルセグメントを読み込み: " + bytesRead + " bytes");
            return buffer;

        } catch (IOException e) {
            System.err.println("ファイル読み込み中にエラー: " + e.getMessage());
            return new byte[0];
        }
    }

    public static void main(String[] args) throws IOException {
        // テスト用のファイル作成
        byte[] testData = new byte[1000];
        for (int i = 0; i < testData.length; i++) {
            testData[i] = (byte) (i % 256);
        }
        Files.write(Paths.get("test.bin"), testData);

        // 部分読み込みテスト
        byte[] segment = readFileSegment("test.bin", 100, 200);
        System.out.println("読み込んだセグメントサイズ: " + segment.length);
    }
}

上級問題 解答例

問題10: メモリマップトファイルの使用

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;

public class MemoryMappedFileHandler {
    public static void processWithMemoryMapping(String filePath, long fileSize) {
        try (FileChannel channel = FileChannel.open(Paths.get(filePath), 
             StandardOpenOption.READ, StandardOpenOption.WRITE)) {

            // メモリマップトファイルの作成
            MappedByteBuffer buffer = channel.map(
                FileChannel.MapMode.READ_WRITE, 0, fileSize);

            // バッファの操作
            System.out.println("バッファ容量: " + buffer.capacity());
            System.out.println("バッファ位置: " + buffer.position());
            System.out.println("バッファリミット: " + buffer.limit());

            // データの読み書き
            for (int i = 0; i < Math.min(100, fileSize); i++) {
                byte b = buffer.get(i);
                System.out.printf("位置 %d: 値 %d\n", i, b);
            }

            // データの書き込み
            buffer.put(0, (byte) 0xFF);
            buffer.put(1, (byte) 0xAA);

            // 変更をファイルに強制書き込み
            buffer.force();

            System.out.println("メモリマップトファイルの処理が完了しました");

        } catch (IOException e) {
            System.err.println("メモリマップトファイル処理中にエラー: " + e.getMessage());
        } catch (OutOfMemoryError e) {
            System.err.println("メモリ不足: ファイルサイズが大きすぎます");
        }
    }

    public static void createTestFile(String filePath, long size) throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(filePath, "rw")) {
            file.setLength(size);
            // テストデータを書き込み
            for (long i = 0; i < size; i++) {
                file.write((byte) (i % 256));
            }
        }
    }

    public static void main(String[] args) throws IOException {
        createTestFile("large_mapped.bin", 1024 * 1024); // 1MB
        processWithMemoryMapping("large_mapped.bin", 1024 * 1024);
    }
}

問題11: 非同期ファイル入出力

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.util.concurrent.*;

public class AsyncFileProcessor {

    // コールバックを使用した非同期読み込み
    public static void readAsyncWithCallback(String filePath) {
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                Paths.get(filePath), StandardOpenOption.READ)) {

            ByteBuffer buffer = ByteBuffer.allocate(1024);
            long position = 0;

            CompletionHandler handler = 
                new CompletionHandler() {

                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("非同期読み込み完了: " + result + " bytes");

                    if (result > 0) {
                        attachment.flip();
                        byte[] data = new byte[attachment.remaining()];
                        attachment.get(data);
                        System.out.println("読み込んだデータ: " + new String(data));
                        attachment.clear();
                    }
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.err.println("非同期読み込み失敗: " + exc.getMessage());
                }
            };

            channel.read(buffer, position, buffer, handler);
            System.out.println("非同期読み込みを開始しました...");

            // 処理が完了するまで待機
            Thread.sleep(2000);

        } catch (IOException | InterruptedException e) {
            System.err.println("非同期ファイル処理中にエラー: " + e.getMessage());
        }
    }

    // Futureを使用した非同期読み込み
    public static Future readAsyncWithFuture(String filePath) {
        try {
            AsynchronousFileChannel channel = AsynchronousFileChannel.open(
                Paths.get(filePath), StandardOpenOption.READ);

            ByteBuffer buffer = ByteBuffer.allocate(1024);
            Future operation = channel.read(buffer, 0);

            System.out.println("Futureを使用した非同期読み込みを開始...");
            return operation;

        } catch (IOException e) {
            System.err.println("非同期ファイル処理中にエラー: " + e.getMessage());
            return CompletableFuture.completedFuture(-1);
        }
    }

    public static void main(String[] args) throws Exception {
        // テストファイル作成
        Files.write(Paths.get("async_test.txt"), 
                   "Hello Async File I/O!".getBytes());

        // コールバック方式
        System.out.println("=== コールバック方式 ===");
        readAsyncWithCallback("async_test.txt");

        // Future方式
        System.out.println("\n=== Future方式 ===");
        Future future = readAsyncWithFuture("async_test.txt");

        // 他の処理を実行可能
        System.out.println("他の処理を実行中...");
        Thread.sleep(1000);

        // 結果の取得
        if (future.isDone()) {
            int bytesRead = future.get();
            System.out.println("Future完了: " + bytesRead + " bytes 読み込み");
        }
    }
}

問題12: カスタムファイルシステムのシミュレーション

import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

// 仮想ファイルシステム
class VirtualFileSystem {
    private static final Map files = new ConcurrentHashMap<>();
    private static final Map> directories = new ConcurrentHashMap<>();

    static {
        directories.put("/", new HashSet<>());
    }

    public static void createFile(String path, byte[] data) {
        files.put(path, new VirtualFile(path, data));
        String parent = getParentPath(path);
        directories.computeIfAbsent(parent, k -> new HashSet<>()).add(getFileName(path));
    }

    public static VirtualFile getFile(String path) {
        return files.get(path);
    }

    public static boolean deleteFile(String path) {
        VirtualFile removed = files.remove(path);
        if (removed != null) {
            String parent = getParentPath(path);
            Set dirContents = directories.get(parent);
            if (dirContents != null) {
                dirContents.remove(getFileName(path));
            }
            return true;
        }
        return false;
    }

    public static Set listDirectory(String path) {
        return directories.getOrDefault(path, Collections.emptySet());
    }

    private static String getParentPath(String path) {
        int lastSlash = path.lastIndexOf('/');
        return lastSlash > 0 ? path.substring(0, lastSlash) : "/";
    }

    private static String getFileName(String path) {
        int lastSlash = path.lastIndexOf('/');
        return path.substring(lastSlash + 1);
    }
}

// 仮想ファイル
class VirtualFile {
    private final String path;
    private byte[] data;
    private long lastModified;

    public VirtualFile(String path, byte[] data) {
        this.path = path;
        this.data = data != null ? data : new byte[0];
        this.lastModified = System.currentTimeMillis();
    }

    public String getPath() { return path; }
    public byte[] getData() { return data; }
    public void setData(byte[] data) { 
        this.data = data; 
        this.lastModified = System.currentTimeMillis();
    }
    public long getLastModified() { return lastModified; }
    public long getSize() { return data.length; }
}

// カスタムInputStream
class VirtualFileInputStream extends InputStream {
    private final VirtualFile file;
    private int position;
    private boolean closed;

    public VirtualFileInputStream(String path) throws FileNotFoundException {
        this.file = VirtualFileSystem.getFile(path);
        if (this.file == null) {
            throw new FileNotFoundException("仮想ファイルが見つかりません: " + path);
        }
        this.position = 0;
        this.closed = false;
    }

    @Override
    public int read() throws IOException {
        if (closed) throw new IOException("ストリームは閉じられています");

        if (position >= file.getSize()) {
            return -1;
        }

        return file.getData()[position++] & 0xFF;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (closed) throw new IOException("ストリームは閉じられています");

        if (position >= file.getSize()) {
            return -1;
        }

        int bytesToRead = Math.min(len, file.getSize() - position);
        System.arraycopy(file.getData(), position, b, off, bytesToRead);
        position += bytesToRead;

        return bytesToRead;
    }

    @Override
    public void close() {
        closed = true;
    }

    @Override
    public int available() throws IOException {
        if (closed) throw new IOException("ストリームは閉じられています");
        return file.getSize() - position;
    }
}

// カスタムOutputStream
class VirtualFileOutputStream extends OutputStream {
    private final VirtualFile file;
    private final boolean append;
    private ByteArrayOutputStream buffer;
    private boolean closed;

    public VirtualFileOutputStream(String path) {
        this(path, false);
    }

    public VirtualFileOutputStream(String path, boolean append) {
        this.file = VirtualFileSystem.getFile(path);
        this.append = append;
        this.buffer = new ByteArrayOutputStream();
        this.closed = false;

        if (file != null && append) {
            buffer.write(file.getData(), 0, file.getData().length);
        }
    }

    @Override
    public void write(int b) throws IOException {
        if (closed) throw new IOException("ストリームは閉じられています");
        buffer.write(b);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        if (closed) throw new IOException("ストリームは閉じられています");
        buffer.write(b, off, len);
    }

    @Override
    public void close() throws IOException {
        if (!closed) {
            if (file != null) {
                file.setData(buffer.toByteArray());
            } else {
                throw new IOException("ファイルが存在しません");
            }
            closed = true;
            buffer.close();
        }
    }

    @Override
    public void flush() throws IOException {
        if (closed) throw new IOException("ストリームは閉じられています");
        // 仮想ファイルシステムでは即時反映
        if (file != null) {
            file.setData(buffer.toByteArray());
        }
    }
}

// メインクラス
public class CustomFileSystemDemo {
    public static void main(String[] args) {
        try {
            // 仮想ファイルの作成
            VirtualFileSystem.createFile("/test.txt", "Hello Virtual File System!".getBytes());
            VirtualFileSystem.createFile("/data/config.properties", "key=value".getBytes());

            // ディレクトリ一覧表示
            System.out.println("ルートディレクトリ: " + VirtualFileSystem.listDirectory("/"));
            System.out.println("/data ディレクトリ: " + VirtualFileSystem.listDirectory("/data"));

            // InputStreamを使用した読み込み
            try (VirtualFileInputStream in = new VirtualFileInputStream("/test.txt")) {
                byte[] buffer = new byte[1024];
                int bytesRead = in.read(buffer);
                System.out.println("読み込んだ内容: " + new String(buffer, 0, bytesRead));
            }

            // OutputStreamを使用した書き込み
            try (VirtualFileOutputStream out = new VirtualFileOutputStream("/test.txt", true)) {
                out.write("\nAppended text!".getBytes());
            }

            // 書き込み後の確認
            try (VirtualFileInputStream in = new VirtualFileInputStream("/test.txt")) {
                byte[] buffer = new byte[1024];
                int bytesRead = in.read(buffer);
                System.out.println("追記後の内容: " + new String(buffer, 0, bytesRead));
            }

            // ファイル削除
            VirtualFileSystem.deleteFile("/data/config.properties");
            System.out.println("削除後の /data ディレクトリ: " + VirtualFileSystem.listDirectory("/data"));

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