概要

銀行の全銀フォーマット、EDI の固定長電文、メインフレーム連携のデータ交換など、固定長ファイルは旧来のシステム連携でいまだに現役です。フィールドの開始位置と長さが仕様書で決められており、カンマやタブのような区切り文字は使いません。Java では substring() でフィールドを切り出し、String.format() でゼロ埋めやスペース埋めのフォーマットを行うのが基本ですが、バイト数と文字数の違いや、トリム処理の漏れなど、実装時に踏みやすい落とし穴があります。この記事では、固定長レコードの読み書きを record 型で型安全に実装し、実務でそのまま使えるパターンを整理します。

使いどころ

銀行の全銀フォーマット(振込データ)を Java で生成し、ファイル転送する

メインフレームから送られてくる固定長レコードを読み込み、DB に取り込む

EDI の電文を固定長フォーマットに変換して取引先に送信する

コード例

固定長レコードの読み書き
import java.io.BufferedReader;

/**
 * 固定長ファイルの読込・書込。
 * レコード形式(1行48文字):
 *   [0-3]   社員番号  4桁  右詰め・スペース埋め
 *   [4-23]  氏名     20桁  左詰め・スペース埋め
 *   [24-31] 部署      8桁  左詰め・スペース埋め
 *   [32-39] 給与      8桁  右詰め・ゼロ埋め
 *   [40-47] 入社日    8桁  yyyyMMdd
 */
public class FixedLengthRecords {

    record EmployeeRecord(int id, String name, String department,
                          int salary, String joinDate) {}

    /** レコードを固定長1行にフォーマット */
    public static String format(EmployeeRecord rec) {
        return "%4d".formatted(rec.id())
             + "%-20s".formatted(rec.name())
             + "%-8s".formatted(rec.department())
             + "%08d".formatted(rec.salary())
             + rec.joinDate();
    }

    /** 固定長1行をレコードにパース */
    public static EmployeeRecord parse(String line) {
        var id         = Integer.parseInt(line.substring(0,  4).trim());
        var name       = line.substring(4,  24).trim();
        var department = line.substring(24, 32).trim();
        var salary     = Integer.parseInt(line.substring(32, 40).trim());
        var joinDate   = line.substring(40, 48).trim();
        return new EmployeeRecord(id, name, department, salary, joinDate);
    }

    public static void main(String[] args) throws IOException {
        var records = List.of(
            new EmployeeRecord(1, "Yamada Taro", "DEV", 450000, "20200401"),
            new EmployeeRecord(2, "Suzuki Hanako", "SALES", 380000, "20210601"),
            new EmployeeRecord(1234, "Tanaka Jiro", "HR", 320000, "20220401")
        );

        var sb = new StringBuilder();
        for (var rec : records) {
            sb.append(format(rec)).append(System.lineSeparator());
        }
        var fileContent = sb.toString();
        System.out.println("=== 固定長フォーマット ===");
        System.out.print(fileContent);

        System.out.println("\n=== パース結果 ===");
        try (var br = new BufferedReader(new StringReader(fileContent))) {
            String line;
            while ((line = br.readLine()) != null) {
                if (!line.isBlank()) {
                    System.out.println(parse(line));
                }
            }
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record(Java 16+)で不変なレコードクラスを簡潔に定義できる。var で型推論も使え、コード量が大幅に減る。

Java 17
// Java 17: record で簡潔に定義
record EmployeeRecord(int id, String name,
    String department, int salary, String joinDate) {}

String formatted = "%4d".formatted(rec.id())
    + "%-20s".formatted(rec.name());

Library Comparison

Pure Java(substring + String.format)フィールド数が少なく、仕様が固定されている場合。外部依存を増やしたくないとき。フィールド数が増えるとコードが冗長になる。仕様変更時の修正箇所が分散しやすい。
Fixedformat4jアノテーションベースで固定長レコードをマッピングしたい場合。メンテナンス状況を確認する必要がある。依存ライブラリの追加が必要。
自前のフィールド定義テーブルフィールド定義を外部ファイル化して仕様変更に柔軟に対応したい場合。フレームワーク的な実装が必要になり、初期構築コストが上がる。

注意点

日本語などのマルチバイト文字を含む場合は、文字数ではなくバイト数でフィールド位置を管理する必要がある。getBytes(charset).length でバイト長を確認すること。

substring() で切り出した値は必ず trim() すること。スペース埋めのフィールドをそのまま parseInt() すると NumberFormatException が発生する。

固定長ファイルでは改行コードの扱いが仕様によって異なる(CRLF / LF / 改行なし)。仕様書に従って改行コードを設定すること。

右詰めゼロ埋めのフォーマット(%08d)で負の値を渡すと、マイナス記号分だけ桁あふれする。入力値の妥当性を事前にチェックすること。

FAQ

文字数とバイト数のどちらで位置を管理すべきですか。

仕様書に従いますが、全銀フォーマットなど多くの業務仕様はバイト数ベースです。Shift_JIS では全角1文字=2バイトで計算します。

フィールドが仕様の桁数を超えた場合はどうしますか。

書き込み前にバリデーションを行い、桁あふれする場合はエラーとして処理するのが安全です。無言で切り捨てると障害の原因になります。

record と通常のクラスのどちらで定義すべきですか。

Java 16 以降なら record を推奨します。不変性が保証され、equals/hashCode/toString が自動生成されるため、データ保持に適しています。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます