StringUtils#split(String, String)の仕様を勘違いしてハマった

Apache Commons Langの StringUtils#split(String, String) の挙動で地味にハマったのでメモ。

環境

Java 1.8.0_211、Apache Commons Lang 3.9にて確認。

状況

StringUtils#split(String, String) の第2引数にCRLFを渡すと、CRでもLFでも分割される。

import static org.apache.commons.lang3.StringUtils.*;

// AssertJでテスト
String CRLF = CR + LF;
assertThat(StringUtils.split(1 + CR + 2, CRLF)).containsExactly("1", "2");
assertThat(StringUtils.split(1 + LF + 2, CRLF)).containsExactly("1", "2");
assertThat(StringUtils.split(1 + CRLF + 2, CRLF)).containsExactly("1", "2");

調査と対応

先入観から、 String#split(String) のNull回避版だろうと思っていたら、仕様は全く違っていた。

https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html を見るとわかるが、第2引数は「separatorChars」、つまり1文字からなるセパレーターを、複数個文字列として渡しているという挙動。

その他、 String#split との違いは以下。

  • String#split では引数は正規表現として扱われるが、 StringUtils#split では単なる文字列として扱われる
  • セパレーターが連続した場合、 String#split では配列に空文字が含まれるが、 StringUtils#split では空文字が含まれない

StringUtils.split(1 + CRLF + 2, CRLF) とした場合、CRで分割、LFで分割、と2回分割されるが、セパレーターが連続しても空文字にならないため、CRLFで分割しているように見えていた。

文字列全体をセパレーターとして使いたい場合、 StringUtils#splitByWholeSeparator(String, String) を使う。

こちらも正規表現ではなく、空文字も含まれない。

また、セパレーターが連続した場合に空文字にしたい場合、 splitPreserveAllTokenssplitByWholeSeparatorPreserveAllTokens といった、「PreserveAllTokens」付きのメソッドを使用できる。

感想

既存処理の挙動を確認していた時に発見し、なんでだろうと思って調べたら結構驚いた。

この仕様を理解して使っていたなら、書いた人はすごいな~と思っていたが、それ以外の場所で "|||" のような区切り文字で結合した文字列を、 StringUtils.split(結合した文字列, "|||"); とかやっていたので、たまたまうまく動いていただけっぽい。

使い慣れているクラスでも、たまにはJavadoc見返したりしないとな、と思った。

備考

同じようにハマった人がいたみたい。とても詳しく記載してくれている。

StringUtilsのsplit系メソッドの違いについて - Qiita