Java Tips | I/O・ネットワーク:ファイル書き込み

Java Java
スポンサーリンク

業務で使える「ファイル書き込みユーティリティ」とは何か

業務システムでは、ログ出力、CSVエクスポート、レポート生成、一時ファイルの保存など、「ファイルに書く」場面が必ず出てきます。 そのたびに FileOutputStreamBufferedWriter を生で書いていると、文字コードや追記・上書きの扱い、例外処理がバラバラになりがちです。

だからこそ、「ファイル書き込み」を小さなユーティリティとしてまとめておく価値があります。 目的は、次のようなことです。

同じパターンの書き込み処理を、毎回コピペしない。 文字コードや改行コード、追記/上書きの方針をプロジェクト全体で統一する。 セキュリティ的に危ない書き込み(意図しない場所への出力など)を避ける。

ここから、初心者向けに一つずつ噛み砕いていきます。

「文字列を丸ごとファイルに書く」基本ユーティリティ

Files.write を使ったシンプルな形

まずは一番よくある、「テキストをファイルに書き出す」ユーティリティから始めます。 java.nio.file.Files を使うと、かなりシンプルに書けます。

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

public class FileWriteUtils {

    public static void writeString(Path path, String content, Charset charset) throws IOException {
        byte[] bytes = content.getBytes(charset);
        Files.write(path, bytes);
    }
}
Java

使い方の例です。

import java.nio.charset.StandardCharsets;
import java.nio.file.Path;

public class WriteExample {

    public static void main(String[] args) throws Exception {
        Path path = Path.of("out/report.txt");
        String text = "レポートの内容です。\n2行目のテキストです。";
        FileWriteUtils.writeString(path, text, StandardCharsets.UTF_8);
    }
}
Java

ここで重要なのは、「文字コード(Charset)を必ず指定する」ことです。 content.getBytes() のように文字コードを省略すると、環境依存になり、 本番と検証環境で文字化けしたり、ログが読めなくなったりします。 業務システムでは、ほぼ必ず UTF-8 に統一するのがおすすめです。

行単位で書くユーティリティ(CSV やログ向け)

「1行ずつ書きたい」場合の基本形

CSV やログなど、「行ごとに書きたい」ケースでは、BufferedWriter を使うのが定番です。 ユーティリティとしてまとめると、次のような形になります。

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

public class LineWriteUtils {

    public static void writeLines(Path path, Iterable<String> lines, Charset charset) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(path, charset)) {
            for (String line : lines) {
                writer.write(line);
                writer.newLine(); // 改行を統一
            }
        }
    }
}
Java

使い方の例です。

import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;

public class LineWriteExample {

    public static void main(String[] args) throws Exception {
        Path path = Path.of("out/users.csv");
        List<String> lines = List.of(
                "id,name,email",
                "1,山田太郎,taro@example.com",
                "2,鈴木花子,hanako@example.com"
        );

        LineWriteUtils.writeLines(path, lines, StandardCharsets.UTF_8);
    }
}
Java

ここで深掘りしたいポイントは、「改行コードをユーティリティ側で統一している」ことです。 writer.newLine() は、OS に応じた適切な改行を出してくれますが、 プロジェクトとして「LF 固定にしたい」などの方針があるなら、"\n" を明示的に使うなど、ルールを決めておくとよいです。

追記(append)と上書き(overwrite)の違いを意識する

追記したいときのユーティリティ

ログや履歴ファイルなど、「既存の内容を残したまま末尾に追加したい」ケースでは、追記モードが必要です。 Files.newBufferedWriter にオプションを渡すことで実現できます。

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;

public class AppendWriteUtils {

    public static void appendLine(Path path, String line, Charset charset) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(
                path,
                charset,
                StandardOpenOption.CREATE,
                StandardOpenOption.APPEND
        )) {
            writer.write(line);
            writer.newLine();
        }
    }
}
Java

使い方の例です。

import java.nio.charset.StandardCharsets;
import java.nio.file.Path;

public class AppendExample {

    public static void main(String[] args) throws Exception {
        Path path = Path.of("out/app.log");
        AppendWriteUtils.appendLine(path, "アプリ起動しました", StandardCharsets.UTF_8);
        AppendWriteUtils.appendLine(path, "処理が完了しました", StandardCharsets.UTF_8);
    }
}
Java

ここで重要なのは、「CREATE と APPEND を両方指定している」ことです。 ファイルが存在しなければ新規作成し、存在すれば末尾に追記する、という挙動になります。 業務ログや履歴ファイルでは、このパターンがよく使われます。

大きなデータを書き出すときの注意点

一気にメモリに載せず、ストリームで書く

大量のデータ(何十万行のCSVなど)を書き出すときに、 「全部を String にまとめてから writeString で一気に書く」やり方をすると、メモリを無駄に使います。

その場合は、「1行ずつ生成しながら書く」スタイルが安全です。

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;

public class StreamingCsvWriter {

    public interface RowProvider {
        Iterable<String> rows();
    }

    public static void writeCsv(Path path, Charset charset, RowProvider provider) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(path, charset)) {
            for (String row : provider.rows()) {
                writer.write(row);
                writer.newLine();
            }
        }
    }
}
Java

ここで深掘りしたいのは、「書き込み処理もストリームとして設計する」という感覚です。 読み込みと同じく、「全部をメモリに載せるか」「流しながら処理するか」を意識して選べるようになると、 業務データ量が増えても耐えられる設計になります。

パスの扱いと「どこに書いてよいか」のルール

ベースディレクトリをユーティリティに閉じ込める

ファイル書き込みは、「どこに何を書くか」がそのままセキュリティと運用に直結します。 誤ってシステムファイルを上書きしたり、意図しない場所に機密情報を書いてしまうと、重大な事故になります。

そこで、「アプリが書いてよい場所」をユーティリティ側で限定しておく設計が有効です。

import java.nio.file.Path;

public class AppOutputPaths {

    private final Path baseDir;

    public AppOutputPaths(Path baseDir) {
        this.baseDir = baseDir;
    }

    public Path logs(String name) {
        return baseDir.resolve("logs").resolve(name);
    }

    public Path exports(String name) {
        return baseDir.resolve("exports").resolve(name);
    }

    public Path temp(String name) {
        return baseDir.resolve("temp").resolve(name);
    }
}
Java

業務コード側では、「必ず AppOutputPaths 経由でパスを取る」というルールにしておくと、 「どこに書いているか」が明確になり、ディレクトリトラバーサルや誤書き込みを防ぎやすくなります。

例外処理と「失敗したときにどう振る舞うか」

IOException を握りつぶさない

ファイル書き込みでは、IOException がほぼ必ず絡みます。 ディスク容量不足、権限不足、パス不正、途中での書き込み失敗などです。

ユーティリティの設計としては、基本的に「throws IOException で呼び出し側に伝える」形から始めるのが分かりやすいです。

public static void writeString(Path path, String content, Charset charset) throws IOException {
    byte[] bytes = content.getBytes(charset);
    Files.write(path, bytes);
}
Java

呼び出し側では、こう書きます。

try {
    FileWriteUtils.writeString(path, text, StandardCharsets.UTF_8);
    // 正常終了の処理
} catch (IOException e) {
    // ログを出す、ユーザーにエラー表示する、リトライするか判断する、など
}
Java

ここで深掘りしたいのは、「例外を無視しない」ことです。 catch (Exception e) {} のように何もせずに握りつぶすと、 「書けていないのに書けたつもりで次の処理に進む」という危険な状態になります。

業務的に重要なファイル(請求データ、締め処理結果など)を書いている場合は、 失敗時の振る舞い(ロールバック、再試行、運用への通知など)をきちんと設計しておく必要があります。

業務でよくある「ファイル書き込みユーティリティ」の具体例

CSV エクスポートユーティリティ

例えば、「ユーザー一覧を CSV にエクスポートする」ユーティリティを考えてみます。

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class UserCsvExporter {

    public static class User {
        public final int id;
        public final String name;
        public final String email;

        public User(int id, String name, String email) {
            this.id = id;
            this.name = name;
            this.email = email;
        }
    }

    public static void export(Path path, Charset charset, List<User> users) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(path, charset)) {
            writer.write("id,name,email");
            writer.newLine();
            for (User u : users) {
                writer.write(u.id + "," + u.name + "," + u.email);
                writer.newLine();
            }
        }
    }
}
Java

使い方のイメージです。

List<UserCsvExporter.User> users = List.of(
        new UserCsvExporter.User(1, "山田太郎", "taro@example.com"),
        new UserCsvExporter.User(2, "鈴木花子", "hanako@example.com")
);

UserCsvExporter.export(Path.of("out/users.csv"), StandardCharsets.UTF_8, users);
Java

ここでのポイントは、「ヘッダ行も含めてユーティリティ側でフォーマットを決めている」ことです。 業務ロジック側は「ユーザーのリストを渡すだけ」で済み、 CSV の細かい書式(カンマ区切り、改行、文字コードなど)はユーティリティに閉じ込められます。

セキュリティ・運用の観点から見たファイル書き込み

機密情報をどこに書くかを常に意識する

ファイル書き込みは、そのまま「情報の外部出力」です。 ログやエクスポートファイルに、パスワードやクレジットカード情報などの機密情報を誤って書いてしまうと、重大なインシデントになります。

だからこそ、次のような点を意識する必要があります。

ログやCSVに書き出す項目を、設計段階でレビューする。 機密情報はファイルに書かない、もしくは暗号化して書く方針を決める。 出力先ディレクトリの権限(誰が読めるか)を運用とセットで管理する。

ユーティリティ側で「書き出す項目を限定する」「出力先を限定する」といった工夫をしておくと、 業務コード側のセキュリティ負担を減らせます。

長期運用での「ディスク枯渇」を防ぐ

ログやエクスポートファイルを延々と書き続けると、 ディスク容量が枯渇して、システム全体が止まることがあります。

ファイル書き込みユーティリティは、 ローテーション(一定サイズ・一定期間でファイルを切り替える)や、 古いファイルの削除とセットで設計されるべきです。

ここはユーティリティ単体というより、「運用設計」との合わせ技になりますが、 「書きっぱなしにしない」という意識を持っておくことが大事です。

まとめ:ファイル書き込みユーティリティで身につけてほしい感覚

ファイル書き込みユーティリティは、「よくある書き込みパターンを、分かりやすく・安全にまとめる」ための小さな道具です。 そこには、次のような大事なポイントが詰まっています。

文字コード(Charset)を必ず指定し、環境依存を避ける。 行単位の書き込みでは、改行コードをユーティリティ側で統一する。 追記(append)と上書き(overwrite)の違いを意識し、用途に応じて使い分ける。 パスの扱いをユーティリティに閉じ込め、「どこに書いてよいか」を明確にする。 例外を握りつぶさず、失敗時の振る舞い(ログ・通知・再試行など)を設計する。

もしあなたのプロジェクトで、 毎回バラバラに FileOutputStreamBufferedWriter を手書きしている場所が散らばっているなら、 それを一度「ファイル書き込みユーティリティ」に置き換えられないか眺めてみてください。

それだけで、コードの意図がはっきりし、 性能・セキュリティ・運用のバランスが取れた I/O 設計に、一歩近づきます。

タイトルとURLをコピーしました