改行統一は「バラバラな改行コードを“同じルール”にそろえる」技
業務システムを書いていると、いろんなところからテキストが入ってきます。
Windows で作られたファイル、Linux サーバ上のログ、Web から送られてくるテキストエリアの内容…。
ここで地味に効いてくるのが 「改行コードの違い」 です。
Windows:\r\n
Unix/Linux/macOS:\n
古い Mac:\r(ほぼ見ないけど、歴史的には存在)
人間の目には全部「改行」に見えますが、文字列としては別物です。
この違いを放置すると、
- 行数カウントが合わない
- split しても行ごとに分割できない
- 差分比較ツールで「全部違う」と判定される
といった、地味だけど厄介なトラブルになります。
そこで使うのが 改行統一(改行コードの正規化)――
「どんな改行コードが来ても、いったん自分たちのルールにそろえる」テクニックです。
まず「改行コードの種類」をちゃんと知っておく
代表的な3種類の改行コード
改行コードには、主に次の3種類があります。
Windows 形式:\r\n(CR+LF)
Unix 形式:\n(LF)
古い Mac 形式:\r(CR)
Java の文字列の中では、
\r は「キャリッジリターン(CR)」\n は「ラインフィード(LF)」
という1文字として扱われます。
例えば、Windows で作られたテキストファイルを読み込むと、
1行ごとに ... \r\n が末尾についていることになります。
改行統一の目的は、
「これらのバラバラな改行コードを、自分たちが決めた1種類にそろえる」ことです。
基本方針:「全部いったんバラしてから、1種類にそろえる」
置換の順番がとても大事
改行統一の基本的な考え方はこうです。
- まず、複数文字の改行コード(
\r\n)を単一の記号にそろえる - 残った単体の改行コード(
\r)も同じ記号にそろえる - 最終的に「自分たちの標準改行コード」に変換する
ここで一番大事なのが 「置換の順番」 です。
いきなり \r を \n に置き換えてしまうと、\r\n が \n\n になってしまい、改行が2つに増えてしまいます。
なので、必ず 「\r\n → \n」を先にやってから、「残った \r → \n」 の順番で処理します。
この順番を守ることが、改行統一の一番のキモです。
実装:LF(\n)に統一するユーティリティ
内部表現をまず LF にそろえる
まずは、「どんな改行コードが来ても、内部では LF(\n)に統一する」ユーティリティを作ります。
public final class Newlines {
private Newlines() {}
public static String normalizeToLf(String text) {
if (text == null) {
return null;
}
// 1. Windows の CRLF を LF に
String s = text.replace("\r\n", "\n");
// 2. 残っている CR を LF に
s = s.replace("\r", "\n");
return s;
}
}
Java使い方はこうなります。
String windows = "A\r\nB\r\nC\r\n";
String unix = "A\nB\nC\n";
String oldMac = "A\rB\rC\r";
System.out.println(Newlines.normalizeToLf(windows)); // A\nB\nC\n
System.out.println(Newlines.normalizeToLf(unix)); // A\nB\nC\n
System.out.println(Newlines.normalizeToLf(oldMac)); // A\nB\nC\n
Javaここで深掘りしたい重要ポイントは二つです。
一つ目は、「必ず \r\n を先に置換している」ことです。
これを逆にすると、\r\n が \n\n になってしまい、改行が増えてしまいます。
「複数文字の改行コード → 単一文字の改行コード」の順番を守るのが鉄則です。
二つ目は、「内部表現として“LF に統一する”と決めている」ことです。
アプリケーション内部では LF にそろえておき、
ファイルに書き出すときや外部に出すときに、必要なら CRLF に変換する――
この「内部は LF、外部は相手に合わせる」という方針にしておくと、処理がシンプルになります。
実装:任意の改行コードに変換するユーティリティ
「内部は LF → 外に出すときに変換」という二段構え
次に、「LF に統一された文字列」を、任意の改行コードに変換するユーティリティを作ります。
public final class Newlines {
private Newlines() {}
public static String normalizeToLf(String text) {
if (text == null) {
return null;
}
String s = text.replace("\r\n", "\n");
s = s.replace("\r", "\n");
return s;
}
public static String toCrLf(String text) {
String s = normalizeToLf(text);
if (s == null) {
return null;
}
return s.replace("\n", "\r\n");
}
public static String toLf(String text) {
return normalizeToLf(text);
}
}
Java使い方はこうです。
String mixed = "A\r\nB\nC\rD";
System.out.println(Newlines.toLf(mixed)); // A\nB\nC\nD
System.out.println(Newlines.toCrLf(mixed)); // A\r\nB\r\nC\r\nD
Javaここでのポイントは、「“まず normalizeToLf で内部表現にそろえてから、外向けの形式に変換する”という二段構え」です。
- 入力:どんな改行コードでも受け入れる
- 内部:LF に統一して扱う
- 出力:相手の期待する改行コード(LF / CRLF)に変換して渡す
この流れをユーティリティとして固定しておくと、
「どこかだけ CRLF のまま残っていた」「一部だけ \r が混ざっていた」といった事故を防げます。
例題:行単位で処理したいときの「改行統一 → split」
改行統一をしてから split するのが安全
テキストを行単位で処理したいとき、
いきなり split("\n") してしまうと、Windows 形式の \r\n がうまく扱えません。
そこで、「改行統一 → split」という順番にします。
public final class Lines {
private Lines() {}
public static String[] splitLines(String text) {
String normalized = Newlines.normalizeToLf(text);
if (normalized == null || normalized.isEmpty()) {
return new String[0];
}
return normalized.split("\n", -1); // 最後の空行も保持したい場合は -1
}
}
Java使い方はこうです。
String mixed = "A\r\nB\nC\r";
String[] lines = Lines.splitLines(mixed);
for (String line : lines) {
System.out.println("[" + line + "]");
}
// [A]
// [B]
// [C]
// []
Javaここで深掘りしたいポイントは、「改行統一をしてから split することで、“改行コードの違い”を意識せずに行処理ができるようになる」ことです。Lines.splitLines を使う側は、「改行コードが何だったか」を気にせず、
単純に「行の配列」として扱えます。
例題:差分比較やハッシュ計算の前に改行統一する
「見た目は同じなのに“違う”と判定される問題」を防ぐ
テキストの差分比較やハッシュ計算(チェックサム)をするとき、
改行コードが違うだけで「別物」と判定されてしまうことがあります。
例えば、内容は同じだけど、片方は LF、もう片方は CRLF のファイル。
人間が見れば同じですが、バイト列としては違うので、
そのまま比較すると「全部違う」となってしまいます。
これを防ぐには、「比較前に改行統一する」という一手間が効きます。
public final class TextEquals {
private TextEquals() {}
public static boolean equalsIgnoringNewlines(String a, String b) {
String na = Newlines.normalizeToLf(a);
String nb = Newlines.normalizeToLf(b);
if (na == null && nb == null) {
return true;
}
if (na == null || nb == null) {
return false;
}
return na.equals(nb);
}
}
Java使い方はこうです。
String s1 = "A\r\nB\r\nC\r\n";
String s2 = "A\nB\nC\n";
System.out.println(TextEquals.equalsIgnoringNewlines(s1, s2)); // true
Javaここでのポイントは、「“改行コードの違いだけは無視して比較したい”という要件を、ユーティリティとして切り出している」ことです。
これを使えば、「内容は同じなのに改行コードの違いでテストが落ちる」といったストレスを減らせます。
例題:外部システムごとに改行コードを切り替える
「内部は LF、外部は相手に合わせる」実践編
例えば、こんな状況を考えます。
- アプリ内部では LF に統一して扱いたい
- Windows 系の外部システムに送るファイルは CRLF にしたい
- Linux 上の別システムに送るファイルは LF のままでよい
この場合、出力前に「どの改行コードにするか」を切り替えるだけで対応できます。
public enum NewlineStyle {
LF,
CRLF
}
public final class NewlineConverter {
private NewlineConverter() {}
public static String convert(String text, NewlineStyle style) {
switch (style) {
case LF:
return Newlines.toLf(text);
case CRLF:
return Newlines.toCrLf(text);
default:
throw new IllegalArgumentException("Unknown style: " + style);
}
}
}
Java使い方はこうです。
String body = "A\r\nB\nC\r";
String forWindows = NewlineConverter.convert(body, NewlineStyle.CRLF);
String forLinux = NewlineConverter.convert(body, NewlineStyle.LF);
Javaここでのポイントは、「改行コードの違いを“列挙型の値”として扱い、分岐を一箇所に閉じ込めている」ことです。
呼び出し側は「Windows 用なら CRLF、Linux 用なら LF」と指定するだけでよく、
具体的な置換ロジックは Newlines と NewlineConverter に任せられます。
まとめ:改行統一ユーティリティで身につけたい感覚
改行統一は、「人間には同じに見えるけれど、機械にとっては違う“改行コードの揺れ”」を吸収するための、
業務システムではかなり重要なテクニックです。
押さえておきたい感覚は、まず「改行コードには複数種類があり、特に \r\n と \n を区別しないといけない」ということ。
次に、「normalizeToLf のような“内部表現を LF にそろえる”ユーティリティを用意し、その上で toCrLf など外向けの変換を重ねる」という二段構え。
そして、「行処理・差分比較・ハッシュ計算・外部連携など、“改行コードの違いでバグりやすいところ”の前後で、改行統一を一度通す」という設計です。
