Java Tips | 文字列処理:文字列分割

Java Java
スポンサーリンク

文字列分割は「一つの塊を意味ごとに切り出す」技

業務システムでは、CSVの1行、ログ1行、設定ファイルの1行、HTTPヘッダ、クエリ文字列など、「一つの長い文字列を、意味のあるピースに切り分けたい」場面がひたすら出てきます。
ここをなんとなく split で書き始めると、「思った場所で切れない」「空文字が混ざる」「末尾の空要素が消える」「パフォーマンスが落ちる」といった、地味だけど厄介な問題にぶつかります。

だからこそ、「業務でよく使う分割パターンを、ユーティリティとして“型”にしておく」ことが大事になります。
今日はそのための考え方と、初心者でもすぐ使えるコードパターンを、丁寧にかみ砕いていきます。


Java の基本武器 String.split の正体をちゃんと知る

String.split は、Java で一番よく使われる分割メソッドです。

String s = "apple,banana,grape";
String[] parts = s.split(",");
System.out.println(Arrays.toString(parts)); // [apple, banana, grape]
Java

ここまでは直感どおりですが、重要なのは「split の引数は“区切り文字”ではなく“正規表現”として解釈される」という点です。

例えば、次のコードを見てください。

String s = "a.b.c";
String[] result = s.split(".");
System.out.println(Arrays.toString(result));
Java

. で区切るから [“a”, “b”, “c”] かな」と思いきや、実際には空配列になってしまいます。
理由は、正規表現において . は「任意の1文字」を意味する特別な記号だからです。

このように、「.」「|」「*」「+」「?」「[」「]」など、正規表現で意味を持つ文字を区切りに使いたいときは、そのまま split に渡すと壊れます。
ここを安全にするための“お作法”を、ユーティリティに閉じ込めてしまうのが賢いやり方です。


正規表現を意識しなくて済む split ユーティリティを作る

区切り文字列を「正規表現ではなくリテラルとして扱う」

.| を含む区切りでも安全に分割したい」「呼び出し側に正規表現を意識させたくない」というときに使えるのが Pattern.quote です。

import java.util.regex.Pattern;

public final class StringSplitter {

    private StringSplitter() {}

    public static String[] splitLiteral(String text, String delimiter) {
        if (text == null) {
            return new String[0];
        }
        if (delimiter == null || delimiter.isEmpty()) {
            return new String[] { text };
        }
        String regex = Pattern.quote(delimiter);
        return text.split(regex, -1); // -1 は末尾の空要素も残す指定
    }
}
Java

ここで深掘りしたい重要ポイントは二つあります。

一つ目は、「Pattern.quote で区切り文字列を“正規表現としてではなく、ただの文字列として扱う”ようにしている」ことです。
これにより、.| を含む区切りでも、安心して splitLiteral に渡せます。

二つ目は、「split の第二引数に -1 を渡している」ことです。
String.split は、デフォルトだと末尾の空文字列を削ってしまう挙動がありますが、-1 を指定すると末尾の空要素もそのまま残してくれます。
CSV やログなどで「最後のカラムが空かどうか」が意味を持つ場合、この違いはかなり重要です。


例題1:CSV風の1行を安全に分割する

「カンマ区切りの1行を配列にしたい」というのは、業務で最もよく出てくるパターンの一つです。
ここでは、あえて「ダブルクォートによるエスケープは考えない、シンプルな CSV 風」と割り切って、splitLiteral を使ってみます。

public final class CsvLineParser {

    private CsvLineParser() {}

    public static String[] parseSimpleCsvLine(String line) {
        return StringSplitter.splitLiteral(line, ",");
    }
}
Java

使い方はこうなります。

String line = "山田太郎,30,東京,";
String[] cols = CsvLineParser.parseSimpleCsvLine(line);

System.out.println(cols.length);      // 4
System.out.println(cols[0]);         // 山田太郎
System.out.println("\"" + cols[3] + "\""); // ""(空文字)
Java

ここでのポイントは、「末尾のカンマの後ろの“空のカラム”も、ちゃんと配列に残っている」ことです。
split のデフォルト挙動だと、この最後の空要素が消えてしまい、「カラム数が合わない」というバグの原因になります。

もちろん、実際の CSV ではダブルクォートや改行など、もっと複雑なルールがありますが、
「まずはシンプルなケースを安全に扱えるユーティリティを持つ」というのは、十分に実務的な一歩です。


例題2:1文字区切り専用の高速 split を自作する

String.split は便利ですが、「正規表現エンジンを使う」という性質上、頻繁に呼ぶとオーバーヘッドが気になることがあります。
「区切りは1文字で十分」「とにかく速くたくさん分割したい」という場合は、自前でループを書くのも有効です。

import java.util.ArrayList;
import java.util.List;

public final class FastCharSplitter {

    private FastCharSplitter() {}

    public static String[] splitByChar(String text, char delimiter) {
        if (text == null) {
            return new String[0];
        }
        List<String> result = new ArrayList<>();
        int start = 0;
        int len = text.length();
        for (int i = 0; i < len; i++) {
            if (text.charAt(i) == delimiter) {
                result.add(text.substring(start, i));
                start = i + 1;
            }
        }
        result.add(text.substring(start, len)); // 最後の要素
        return result.toArray(new String[0]);
    }
}
Java

使い方はこうです。

String s = "A|B||D";
String[] parts = FastCharSplitter.splitByChar(s, '|');

System.out.println(Arrays.toString(parts)); // [A, B, , D]
Java

ここで深掘りしたいのは、「String.split のように正規表現を解釈しないので、.| などもそのまま区切りとして扱える」ことと、
「正規表現エンジンを使わないぶん、単純なループとして非常に速い」ということです。

業務で大量のログやテキストを処理するとき、「ここは1文字区切りで十分」と分かっている箇所にこうしたユーティリティを使うと、
パフォーマンスと挙動の安定性を両立できます。


例題3:トリムや空要素除去まで含めた「業務用 split」

実務では、「分割したあとに前後の空白を削りたい」「空文字の要素はいらない」といった要件もよく出てきます。
これも毎回呼び出し側で書くのではなく、ユーティリティに閉じ込めてしまいましょう。

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

public final class BusinessStringSplitter {

    private BusinessStringSplitter() {}

    public static String[] splitAndTrimIgnoreEmpty(String text, String delimiter) {
        if (text == null) {
            return new String[0];
        }
        String regex = Pattern.quote(delimiter);
        String[] raw = text.split(regex, -1);
        List<String> result = new ArrayList<>();
        for (String r : raw) {
            if (r == null) {
                continue;
            }
            String trimmed = r.trim();
            if (!trimmed.isEmpty()) {
                result.add(trimmed);
            }
        }
        return result.toArray(new String[0]);
    }
}
Java

使い方はこうです。

String s = "  山田  , ,  佐藤 ,鈴木  ";
String[] names = BusinessStringSplitter.splitAndTrimIgnoreEmpty(s, ",");

System.out.println(Arrays.toString(names)); // [山田, 佐藤, 鈴木]
Java

ここでの重要ポイントは、「“どこまでを分割の責務とするか”をユーティリティ側で決めている」ことです。
このメソッドを使うと、「区切りで分割する」「前後の空白を削る」「空要素を捨てる」という三つの処理を、
呼び出し側は一行で済ませられます。

こうして「業務でよく出るパターン」を名前付きメソッドにしていくと、
コードを読むときに「何をしたいのか」が一瞬で分かるようになり、バグも入り込みにくくなります。


パフォーマンスと設計をどうバランスさせるか

文字列分割は、ログ解析や大量データ処理ではボトルネックになりやすい一方で、
小さな文字列に対してたまに呼ぶ程度なら、String.split のオーバーヘッドはほとんど気になりません。

初心者向けに整理すると、次のような感覚で十分です。

小規模・たまにしか呼ばない処理なら、まずは splitLiteralsplitAndTrimIgnoreEmpty のような「読みやすいユーティリティ」を優先する。
大量データ・高頻度の処理でボトルネックになってきたら、FastCharSplitter のような「用途特化の高速版」を導入する。

そして何より大事なのは、「split を生であちこちに書かず、“分割のルール”をユーティリティに集約する」という設計の姿勢です。
そうしておけば、「末尾の空要素を残したい」「空白を削りたい」「区切りが正規表現と衝突する」といった仕様変更があっても、
ユーティリティを直すだけで、呼び出し側はほぼノータッチで済むようになります。


まとめ:文字列分割ユーティリティで身につけたい感覚

文字列分割は、「一つの塊を意味ごとに切り出す」ための、とても地味だけれど超重要な技です。

押さえておきたいのは、まず「String.split の引数は正規表現であり、.| などはそのまま渡すと壊れる」という事実。
次に、「Pattern.quote や自前ループを使って、“正規表現を意識しなくていい分割”をユーティリティとして用意する」こと。
そして、「トリム・空要素除去・末尾の空要素保持など、“業務で必要なルール”を分割ユーティリティに閉じ込めて、プロジェクト全体で同じ挙動を共有する」ことです。

もしあなたのコードのどこかに、s.split(",")s.split("\\|") が生で散らばっているなら、
その一つを題材にして、ここで作った StringSplitter.splitLiteralBusinessStringSplitter.splitAndTrimIgnoreEmpty に置き換えてみてください。
それだけで、「壊れにくくて、読みやすくて、パフォーマンスもコントロールしやすい文字列分割」に、一段レベルアップできます。

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