🥮

文字列を分割するsplitメソッドの使い方と注意点

2022/11/03に公開

Javaではsplitメソッドを利用することで、文字列を分割することができます。

基本的な使い方

以下のサンプルは、文字列をカンマ (,) で区切り、配列として受け取る処理です。

String target = "apple,melon,banana,grape";
String[] array = target.split(",");

// ["apple", "melon", "banana", "grape"]
for (String element : array) {
  System.out.println(element);
}

splitメソッドで得られる値は配列型です。ただ、配列は機能が乏しく使い勝手が良くありません。分割後のデータは配列型ではなくList型として受け取りたい場面が多いでしょう。

文字列型をList型に変換したい場合、splitメソッドで得られた配列型の値をArrays.asListメソッドの引数に渡すことで実現できます。

import java.util.Arrays;
import java.util.List;

// ...省略...

String target = "apple,melon,banana,grape";
List<String> list = Arrays.asList(target.split(","));

System.out.println(list); // ["apple", "melon", "banana", "grape"]

split メソッドの注意点

特に注意を払う必要のないメソッドのように見えますが、仕様を理解していないと思わぬ落とし穴にハマってしまう恐れがあります。

以降ではsplitメソッドの注意点を説明していきます。

引数の区切り文字は、正規表現として処理される

splitメソッドの引数に指定する区切り文字は、splitメソッドの内部では正規表現として扱われています。参考用にsplitメソッドの内部処理を掲載します。

String.class
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

    public String[] split(String regex, int limit) {

        // ...省略...

        return Pattern.compile(regex).split(this, limit);
    }
}

そのため何かしらの記号を区切り文字として使用する場合には注意が必要です。

例えば、正規表現であることを意識せずにドット (.) で文字列を区切ろうとしても適切なかたちに分割できません。

String target = "192.168.1.0";
String[] result = target.split("."); // []

上記の例では、文字列を分割して得られたデータは空の配列となっております。
ドット (.) は正規表現では「任意の1文字」を表すため、上記の例ではすべての文字が区切り文字とみなされて処理されていることが原因です。

ドット (.) で区切りたい場合には、ドットを\でエスケープするなどの対応が必要です。

String target = "192.168.1.0";
String[] result = target.split("\\."); // ["192", "168", "1", "0"]

正規表現で処理されるという性質を利用して、以下のような処理を実装することもできます。

String target = "apple,melon, banana, grape";
String[] result = target.split(",\\s*"); // ["apple", "melon", "banana", "grape"]

\sは半角スペース、 *は直前の文字が0文字以上という正規表現です。
つまり「カンマ+半角スペース(もし存在するのであれば含める)」で分割していることになります。

この分割方法であれば、カンマの後ろの半角スペースの有無にかかわらず、分割後の配列の各要素には半角スペースが含まれなくなります。

デフォルトでは、配列の末尾の空文字の要素がすべて削除される

splitメソッドの第2引数を指定しないか、あるいは第2引数に0を指定した場合、分割後の配列から末尾の空文字の要素が削除されます

String target = "apple,melon,,banana,,";
// 第2引数を指定しない場合
String[] result = target.split(","); // ["apple", "melon", "", "banana"]

上記の文字列をカンマ区切りで分割した場合、「banana」より後ろにある2つの要素は空文字のため削除されます。
固定の長さを前提としたCSVファイルの行データの読み込み処理などを実装する際には、特に注意が必要な挙動です。

この問題は、splitメソッドの第2引数に0以外の数値を与えることで回避できます。

第2引数に負の値を指定した場合には、以下のように空文字の要素は削除されなくなります

String target = "apple,melon,,banana,,";
String[] result1 = target.split(",", -1); // ["apple", "melon", "", "banana", "", ""]

第2引数に正の値を指定した場合には、指定値を上限に分割処理を行うようになります。
配列の長さが固定で決まっている場合は、その値を指定すると良いでしょう。

ただし、最後の要素には分割されなかった残りの文字列がすべて格納される点には注意が必要です。

String target = "apple,melon,,banana,,";
String[] result4 = target.split(",", 4); // ["apple", "melon", "", "banana,,"]
String[] result5 = target.split(",", 5); // ["apple", "melon", "", "banana", ","]
String[] result6 = target.split(",", 6); // ["apple", "melon", "", "banana", "", ""]
String[] result7 = target.split(",", 7); // ["apple", "melon", "", "banana", "", ""]

私の所感ですが、splitメソッドを使用して文字列を分割する場合、基本的には第2引数に負の値(-1)を指定しておいた方が良い気がします。空文字の要素を削除したい場面はそう多くない印象です。

空文字を分割しても空の配列は得られない

String empty = "";
String[] array = empty.split(","); // [""]

空文字を分割して得られた結果は空配列ではなく、0番目に空文字の要素を持つ配列です。
空文字をsplitメソッドに渡すと空配列が返ってくる...と考えるかもしれませんが、実際にはそのような結果にはなりません。

ちなみに、これはJava独特の仕様というわけではありません。JavaScriptやPythonのsplitも同様の挙動です。

nullを分割しようとすると例外が発生する

nullに対してsplitメソッドを使うことはできません。splitメソッドを利用した場合、例外が発生します。

String target = null;
String[] array = target.split(","); // NullPointerException

汎用的な分割用メソッドを考えてみる

以上の内容を踏まえた汎用的な分割用メソッドを作成してみました。
何かを実装する際にお役に立てれば幸いです。

このメソッドは以下の特徴を備えています。

  • 戻り値は配列型ではなく利便性の高いList型。
  • 分割対象の文字列が空文字の場合、空のListを返す。
  • 分割処理で空文字の要素は削除されない。(デフォルトでsplitの第2引数に負の値がセットされる)
import java.util.Arrays;
import java.util.List;

public class CollectionsUtil {

  private CollectionsUtil() {}

  public static List<String> toList(String target, String delimiter) {
    return toList(target, delimiter, -1);
  }

  public static List<String> toList(String target, String delimiter, int limit) {
    if (target.isEmpty()) {
      return Arrays.asList();
    }
    return Arrays.asList(target.split(delimiter, limit));
  }
}

上記のメソッドの実行結果

List<String> list1 = CollectionsUtil.toList("apple,melon,banana,grape", ","); // ["apple", "melon", "banana", "grape"]
List<String> list2 = CollectionsUtil.toList("apple,melon,,banana,,", ","); // ["apple", "melon", "", "banana", "", ""]
List<String> list3 = CollectionsUtil.toList("apple,melon,,banana,,", ",", 4); // ["apple", "melon", "", "banana,,"]
List<String> list4 = CollectionsUtil.toList("", ","); // []

参考ドキュメント

備考

サードパーティライブラリの中でも人気なGoogle Guavaを利用されているのであれば、Javaの標準ライブラリが提供するsplitメソッドの代わりにGoogle Guavaから提供されているSplitterを利用するという選択肢もあります。
ただし、Splitterは以下のようにsplitメソッドと利用方法が大きく異なりますので、ドキュメントを読むなどして利用方法を理解する必要があります。
splitメソッドをマスターするのとSplitterをマスターするのとでは、どちらの方が学習コストが低いだろうか...?)

import com.google.common.base.Splitter;

// ...省略...

String target = "apple,melon, ,banana,,";
List<String> list = Splitter.on(',')
    .trimResults()
    .omitEmptyStrings()
    .splitToList(target);
// ["apple", "melon", "banana"]

Apache Commonsも文字列を分割する機能を提供しています。
ただしApache Commonsのsplitメソッドは、Javaの標準ライブラリが提供するsplitメソッドとは挙動が異なる点には注意が必要です。

import org.apache.commons.lang3.StringUtils;

// ...省略...

String target = "apple,melon,,banana,,";

// Javaの標準ライブラリのsplit
List<String> list1 = Arrays.asList(target.split(","));
// ["apple", "melon", "", "banana"]

// Apache Commonsのsplit
List<String> list2 = Arrays.asList(StringUtils.split(target, ","));
// ["apple", "melon", "banana"]

Discussion