split()メソッドの基本動作と空文字列問題
JavaのStringクラスにあるsplit()メソッドは、指定された正規表現に基づいて文字列を分割します。しかし、特定の条件下では意図しない空文字列が結果に含まれることがあります。この現象は、Leetcodeの問題151「文字列内の単語を反転させる」などでtrim()メソッドを使用しない場合に問題となります。
split()メソッドのソースコード解析
split()メソッドのソースコードを詳細に見てみましょう。以下は簡略化されたバージョンです。
public String[] split(String regex, int limit) {
char ch = 0;
if (regex.length() == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) {
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else {
int last = length();
list.add(substring(off, last));
off = last;
break;
}
}
if (off == 0)
return new String[]{this};
if (!limited || list.size() < limit)
list.add(substring(off, length()));
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).isEmpty()) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
高速パス処理の条件
最初のif文では、高速パス処理が適用される条件を判定しています。正規表現が単一文字で、かつメタ文字(".$|()[{^?*+\\")でない場合に高速パスが利用されます。
文字列分割のロジック
分割処理では、offが現在の検索開始位置、nextが一致位置を示します。indexOfメソッドで一致位置を検索し、substringメソッドで部分文字列を抽出しています。
問題となるのは、offとnextが同じ位置にある場合です。この場合、substring(off, next)は空文字列を返し、それが結果リストに追加されます。
limit=0の場合の特別処理
limitパラメータが0の場合、結果の末尾にある空文字列は自動的に削除されます。この仕組みにより、末尾の連続する分割文字の後ろにある空文字列は結果に含まれません。
空文字列生成の具体例
以下に、空文字列が生成される具体例を挙げます。
例1: 文字列の先頭に分割文字がある場合
String text = "!!Hello, World!!";
String[] result = text.split("!");
System.out.println(Arrays.toString(result));
出力結果:
["", "", "Hello, World", "", ""]
文字列の先頭に2つの"!"があるため、分割結果の先頭に2つの空文字列が生成されます。
例2: 文字列の末尾に分割文字がある場合
String text = "Hello, World!!";
String[] result = text.split("!");
System.out.println(Arrays.toString(result));
出力結果:
["Hello, World", "", ""]
文字列の末尾に"!"があるため、分割結果の末尾に空文字列が生成されます。
例3: 文字列の先頭と末尾に分割文字がある場合
String text = "!Hello, World!";
String[] result = text.split("!");
System.out.println(Arrays.toString(result));
出力結果:
["", "Hello, World", ""]
文字列の両端に"!"があるため、分割結果の両端に空文字列が生成されます。
例4: limitパラメータの影響
String text = "A,B,C,,";
String[] result1 = text.split(",");
String[] result2 = text.split(",", 3);
String[] result3 = text.split(",", 5);
System.out.println("limit未指定: " + Arrays.toString(result1));
System.out.println("limit=3: " + Arrays.toString(result2));
System.out.println("limit=5: " + Arrays.toString(result3));
出力結果:
limit未指定: ["A", "B", "C", "", ""]
limit=3: ["A", "B", "C,,"]
limit=5: ["A", "B", "C", "", ""]
limitパラメータを指定することで、分割の回数を制限できますが、空文字列が生成される条件は変わりません。
対策とベストプラクティス
空文字列の問題を回避するためには、以下の対策が有効です:
- 分割前にtrim()メソッドで文字列の前後の空白を削除する
- 正規表現で空文字列を除外する(例: split("\\s+"))
- 分割後に空文字列をフィルタリングする(例: Arrays.stream(result).filter(s -> !s.isEmpty()).toArray(String[]::new))
特にLeetcodeのような問題では、入力文字列の前後の不要な文字を事前に処理することが重要です。