java-recipes

ホーム ファイルI/O › F-07

F-07: 固定長ファイルの読込・書込

固定長ファイルとは、各フィールドのバイト(または文字)位置があらかじめ決まっているテキスト形式です。 帳票システム・銀行間決済(全銀ネット)・EDI など、日本の業務システムで広く使われています。 外部ライブラリ不要で、String.substring()String.format() だけで実装できます。

固定長フォーマットとは

以下のレコード形式を例に説明します。各フィールドは文字位置(開始〜終了)で定義されており、 値が短い場合はスペースまたはゼロで埋めます。

位置    幅    フィールド名    整形ルール
[0-3]    4    社員番号        右詰め・スペース埋め
[4-23]  20    氏名            左詰め・スペース埋め
[24-31]  8    部署コード      左詰め・スペース埋め
[32-39]  8    給与            右詰め・ゼロ埋め
[40-47]  8    入社日          yyyyMMdd 形式
─────────────────────────────────────────
合計 48 文字/行
   1Yamada Taro        DEV     0045000020200401
   2Suzuki Hanako      SALES   0038000020210601
1234Tanaka Jiro        HR      0032000020220401

サンプルコード

Sample.java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * 固定長ファイルの読込・書込サンプル(Java 8+)。
 *
 * レコード形式(1行48文字):
 *   [0-3]   社員番号   4桁  右詰め・スペース埋め
 *   [4-23]  氏名      20桁  左詰め・スペース埋め
 *   [24-31] 部署コード  8桁  左詰め・スペース埋め
 *   [32-39] 給与       8桁  右詰め・ゼロ埋め
 *   [40-47] 入社日     8桁  yyyyMMdd 形式
 *
 * 注意: 日本語などのマルチバイト文字を含む場合は、
 *       文字数ではなくバイト数で位置を管理する必要があります。
 *       このサンプルは ASCII 文字(半角英数)を前提としています。
 */
public class FixedLengthSample {

    static class EmployeeRecord {
        final int    id;
        final String name;
        final String department;
        final int    salary;
        final String joinDate; // yyyyMMdd

        EmployeeRecord(int id, String name, String department,
                int salary, String joinDate) {
            this.id         = id;
            this.name       = name;
            this.department = department;
            this.salary     = salary;
            this.joinDate   = joinDate;
        }

        @Override
        public String toString() {
            return "EmployeeRecord{id=" + id + ", name=" + name
                + ", dept=" + department + ", salary=" + salary
                + ", joinDate=" + joinDate + "}";
        }
    }

    // ---- 書き込み ----

    /**
     * レコードを固定長1行にフォーマットする。
     *
     * String.format() のフォーマット指定:
     *   %4d    → 幅4・右詰め整数
     *   %-20s  → 幅20・左詰め文字列(右側をスペース埋め)
     *   %08d   → 幅8・ゼロ埋め整数
     */
    public static String format(EmployeeRecord rec) {
        return String.format("%4d",  rec.id)          // [0-3]   社員番号(右詰め)
             + String.format("%-20s", rec.name)        // [4-23]  氏名(左詰め)
             + String.format("%-8s",  rec.department)  // [24-31] 部署(左詰め)
             + String.format("%08d",  rec.salary)      // [32-39] 給与(ゼロ埋め)
             + rec.joinDate;                            // [40-47] 入社日
    }

    public static String write(List<EmployeeRecord> records) throws IOException {
        StringWriter sw = new StringWriter();
        BufferedWriter bw = new BufferedWriter(sw);
        for (EmployeeRecord rec : records) {
            bw.write(format(rec));
            bw.newLine();
        }
        bw.flush();
        return sw.toString();
    }

    // ---- 読み込み ----

    /**
     * 固定長1行の文字列からレコードを復元する。
     * substring(start, end) でフィールドを切り出し、trim() で空白を除去する。
     */
    public static EmployeeRecord parse(String line) {
        int    id         = Integer.parseInt(line.substring(0,  4).trim());
        String name       = line.substring(4,  24).trim();
        String department = line.substring(24, 32).trim();
        int    salary     = Integer.parseInt(line.substring(32, 40).trim());
        String joinDate   = line.substring(40, 48).trim();
        return new EmployeeRecord(id, name, department, salary, joinDate);
    }

    /**
     * 固定長ファイルを1行ずつ読み込む。
     * BufferedReader で1行ずつ処理するため、大容量ファイルでもメモリ効率が良い。
     */
    public static List<EmployeeRecord> read(String fileContent) throws IOException {
        List<EmployeeRecord> records = new ArrayList<>();
        BufferedReader br = new BufferedReader(new StringReader(fileContent));
        String line;
        while ((line = br.readLine()) != null) {
            if (!line.isEmpty()) {
                records.add(parse(line));
            }
        }
        return records;
    }

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

        System.out.println("=== 固定長フォーマット ===");
        String fileContent = write(originals);
        System.out.print(fileContent);
        // 出力例:
        //    1Yamada Taro        DEV     00450000 20200401
        //    2Suzuki Hanako      SALES   00380000 20210601
        // 1234Tanaka Jiro        HR      00320000 20220401

        String firstLine = fileContent.split(System.lineSeparator())[0];
        System.out.println("1行の文字数: " + firstLine.length()); // 48

        System.out.println("\n=== パース結果 ===");
        for (EmployeeRecord rec : read(fileContent)) {
            System.out.println(rec);
        }
    }
}

よくあるミス・注意点

⚠️ 日本語(マルチバイト文字)は文字数 ≠ バイト数

「山田太郎」(4文字)は UTF-8 で 12 バイト、Shift-JIS で 8 バイトです。 固定長ファイルの仕様書が「バイト数」で定義されている場合、substring()(文字数ベース)ではなくArrays.copyOfRange(bytes, start, end)(バイト数ベース)で処理が必要です。

⚠️ 改行コードに注意(\r\n vs \n

Windows(\r\n)と Unix(\n)では改行コードが異なります。 金融系の固定長ファイルは CRLF(\r\n)が標準的なことが多いです。System.lineSeparator() はプラットフォーム依存なので、 仕様に合わせて "\r\n" または "\n" を明示してください。

⚠️ フォーマット指定で文字数が超過すると切り捨てられない

String.format("%-20s", "名前が20文字を超える長い名前......") のように 値が定義幅を超えると、切り捨てられずそのまま出力されます。 後続フィールドの位置がずれてしまうため、書き込み前に長さチェックが必要です。

テストする観点

  • ✅ ラウンドトリップ:write() した文字列を read() して元のデータと一致するか
  • ✅ フィールド幅:1行の文字数が仕様通り(48文字)か
  • ✅ 右詰め:id=1" 1"(3スペース+1)になるか
  • ✅ ゼロ埋め:salary=450000"00450000" になるか
  • ✅ 左詰め:name="Yamada"(6文字)が "Yamada "(14スペース追加)になるか
  • ✅ 境界値:id=9999(最大4桁)が正しく処理されるか
  • ✅ 空行スキップ:ファイルに空行が含まれても正しく読み込めるか

GitHub でソースコードを見る →