Android

android.text.TextUtils のメソッドを調べる

概要

勉強のため、android.text.TextUtils について、どんなメソッドがあるのかを調べてみました。

何ができるクラスなのか?

文字列処理で便利なメソッドが揃っています。null の際の比較を柔軟にできるメソッドがいくつかある(equals, isEmpty 等)のが特徴的です。

なぜ commons-lang3 の StringUtils があるのにそちらを使わないのか?

あくまで個人の見解ですが、Android アプリには 64kのメソッド数制限があるので、
メソッド数が多くなる commons-lang3 を気軽に使えないという事情があるのではないかと思います。

なお、皆無というわけではなく、commons-lang3 を採用している Android アプリもあるようです。

参考: https://docs.google.com/presentation/d/11pCsJmahMIP9Gldt39nPnd0XX4lrvycpuNibPmDQ-SM/pub?start=false&loop=false&delayms=3000#slide=id.p6

注意点

このクラスは Android SDK のものであるため、これを用いたクラスの JUnit テストコードを書く際は Robolectric が必要となります。

app/build.gradle
dependencies {
    // dependencies...

    testCompile 'junit:junit:4.12'
    testCompile "org.robolectric:robolectric:3.4.2"
}
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public class UsingTextUtilsClassTest {
// ...

参照したコード

今回は GitHub リポジトリにあった 8.0.0_RC4 のコードを参照しました。

https://github.com/android/platform_frameworks_base/blob/android-8.0.0_r4/core/java/android/text/TextUtils.java

実行環境

項目
Android Studio 2.3.3
compileSdkVersion 25
targetSdkVersion 25
Java 1.8.0_131

メソッド

static CharSequence concat(CharSequence... text)

渡された複数の文字列を連結した結果を返します。

以下の場合

TextUtils.concat("a", "b", "c")

"abc" という文字列が得られます。

可変長引数なので1つも引数を渡さないで呼び出すことが可能です。その場合は空文字列が返ります。

TextUtils.concat();

まあ、何の意味もないので明示的にこのような呼び出しをすることはないでしょう。

static boolean equals(CharSequence a, CharSequence b)

渡された2つの文字列が等しいなら true を返します。このメソッドを使うと片方が null なら false を返すだけで、「うっかり null の方から equals 呼び出して NullPointerException 出しちゃった!」という Java あるあるを回避できます。
また、よくある Java のイディオムで、絶対 null にならない方(主に定数)から equals を呼び出す(ヨーダ記法というらしいです)際にありがちな可読性・美しさの低下を避けることもできます。

ちなみにこのメソッド、単にnullチェックしてから equals を呼んでいるだけかと思ったら、ちゃんとしっかり比較処理を作っているようです。

    public static boolean equals(CharSequence a, CharSequence b) {
        if (a == b) return true;
        int length;
        if (a != null && b != null && (length = a.length()) == b.length()) {
            if (a instanceof String && b instanceof String) {
                return a.equals(b);
            } else {
                for (int i = 0; i < length; i++) {
                    if (a.charAt(i) != b.charAt(i)) return false;
                }
                return true;
            }
        }
        return false;
    }

expandTemplate(CharSequence template, CharSequence... values)

毎回 Formatter オブジェクトを生成する String#format を使いがちなところで、このメソッドを使うとよさそうに思えます。

第1引数の文字列で ^番号 の形式でテンプレートを指定し、続く可変長引数で入れ込む値を指定します。

TextUtils.expandTemplate("^1 is ^2.", "Ice cream", "cool"); // Ice cream is cool.

テンプレート内は必ずしも1から順に置く必要はありません。

TextUtils.expandTemplate("^2 is ^1.", "Ice cream", "cool"); // cool is Ice cream.

ただ、ややこしくなるので通常は順序通りに指定した方がよいかと思います。

同じ番号を複数回用いることもできます。

TextUtils.expandTemplate("^1 is ^1.", "Ice cream", "cool");

第2引数は可変長引数であるため、何も渡さないことができます。その場合は第1引数の文字列がそのまま返されます。

TextUtils.expandTemplate("Ice cream is cool."));

この場合、第1引数の "Ice cream is cool." が返ってきます。何の意味もないのでやる必要はないでしょう。

注意

個数

渡した個数が2なのにテンプレートで3以上の数字を使うと例外 IllegalArgumentException が発生します。

TextUtils.expandTemplate("^3 is ^1.", "Ice cream", "cool");

9個まで

可変長引数ですが、サポートしているのは9個までで、10個以上渡すと IllegalArgumentException が発生します。

static int getLayoutDirectionFromLocale(Locale locale)

APIレベル17 で追加されました。設定言語ごとの文字列の方向を int 定数で返すメソッドです。

方向 定数名 定数値
左から右 LAYOUT_DIRECTION_LTR 0 (0x00000000)
右から左 LAYOUT_DIRECTION_RTL 1 (0x00000001)

static int getTrimmedLength(CharSequence s)

Trim された状態での文字列の長さを返してくれるメソッドです。

TextUtils.getTrimmedLength("a  ");      // 1
TextUtils.getTrimmedLength("a  b");     // 4
TextUtils.getTrimmedLength("  a  b  "); // 4
TextUtils.getTrimmedLength("");         // 0

注意

null を渡すと NullPointerException が発生します。

TextUtils.getTrimmedLength(null);       // NullPointerException

static String htmlEncode(String s)

HTML で困る &lt や &gt を &lt や &gt に置換してくれるメソッドです。

TextUtils.htmlEncode("<a>"); // "&lt;a&gt;"
TextUtils.htmlEncode("");    // ""

注意

null を渡すと NullPointerException が発生します。

assertEquals(null, TextUtils.htmlEncode(null));

static int indexOf(CharSequence s, char ch)

文字列から対象の1文字が含まれる位置を返すメソッドで、いくつかオーバーロードされています。

static int indexOf(CharSequence s, char ch, int start, int end)
static int indexOf(CharSequence s, CharSequence needle, int start, int end)
static int indexOf(CharSequence s, CharSequence needle, int start)
static int indexOf(CharSequence s, CharSequence needle)
static int indexOf(CharSequence s, char ch, int start)

static boolean isDigitsOnly(CharSequence str)

文字列の中が数字だけであれば true を返すメソッドです。

TextUtils.isDigitsOnly("123456"); // true
TextUtils.isDigitsOnly("123g");   // false
TextUtils.isDigitsOnly("");       // false

注意

あまりこういうメソッドを濫用しすぎると、文字列で何でもやってしまう癖がつくので気を付けたいところです。

null を渡すと NullPointerException が発生します。

TextUtils.isDigitsOnly(null);     // NullPointerException!!!

static boolean isEmpty(CharSequence str)

文字列が null または長さ0の時に true を返すメソッドです。 NullPointerException が発生しません。

なお、 Apache の StringUtils と異なり、 isNotEmpty (Empty でないなら true)
isBlank (Emptyまたは空白のみの文字列なら true)
isNoneEmpty (引数すべてが Empty でないなら true)は一切ありません。

static String join(CharSequence delimiter, Iterable tokens)

指定した delimiter を使って Iterable(List や Set等) の要素の toString() した文字列を連結し、その結果を返します。
StringBuilder を使って実装しているようです。この手のコードはよく実装するので手間を省けます。

第1引数の delimiter に null を渡してもそのまま連結できます。

TextUtils.join("", new String[]{"a", "b", "c", "d", "e"});   // "abcde"
TextUtils.join(",", new String[]{"a", "b", "c", "d", "e"});  // "a,b,c,d,e"
TextUtils.join(null, new String[]{"a", "b", "c", "d", "e"}); // "anullbnullcnulldnulle"

第2引数 tokens に null を渡すと Ambiguous method call. でコンパイルエラーになります。

static String join(CharSequence delimiter, Object[] tokens)

同名メソッドの配列版です。拡張 for 文とフラグを使って実装しているようです。

static int lastIndexOf(CharSequence s, char ch)

文字列の中で指定した文字列が最後に現れる index を返します。

static int lastIndexOf(CharSequence s, char ch, int last)
static int lastIndexOf(CharSequence s, char ch, int start, int last)

static CharSequence replace(CharSequence template, String[] sources, CharSequence[] destinations)

対象文字列 template の、sources に一致する文字列を destinations で置き換えます。sources と destinations は同じ index の文字列が対応します。ただ、sources と destinations の数は一致している必要はありませんし、一致しなくてもエラーにはならずそのまま実行されます。

以下の場合、対象文字列 "abc" に対し、"a" -> "c", "c" -> "i" の置換が実施されます。

TextUtils.replace("abc", new String[]{"a", "c"}, new CharSequence[]{"c", "i"});

この結果は "cbi" となります。

TextUtils.replace("abcda", new String[]{"a", "c"}, new CharSequence[]{"c", "i"});
TextUtils.replace("abcda", new String[]{"c", "a"}, new CharSequence[]{"i", "c"});
TextUtils.replace("abcda", new String[]{"c", "a"}, new CharSequence[]{"i", "c", "e"});
TextUtils.replace("abcda", new String[]{"c", "a", "e"}, new CharSequence[]{"i", "c"});

上記はいずれも "cbida" が返されます。

static String[] split(String text, Pattern pattern)

文字列を正規表現で String 配列に分割します。第1引数が空文字列の場合は空の String 配列が返ります。
null の場合は NullPointerException が発生します。

text の長さをチェックして0なら空の配列を返し、そうでない場合は text から split メソッドを呼び出す実装でした。

static String[] split(String text, String expression)

文字列を正規表現で String 配列に分割します。第1引数が空文字列の場合は空の String 配列が返ります。
null の場合は NullPointerException が発生します。

text の長さをチェックして0なら空の配列を返し、そうでない場合は pattern から split メソッドを呼び出す実装でした。

TextUtils.split("to/be/or/not/to", "/"); // [to, be, or, not, to]

注意

第2引数 expression に null を渡すと Ambiguous method call. でコンパイルエラーになります。

TextUtils.split("to/be/or/not/to", null);

static String substring(CharSequence source, int start, int end)

元の文字列から新たな部分文字列を生成して取得します。

TextUtils.substring("tomato", 1, 3); // "om"

注意

source が null だと NullPointerException が発生します。

TextUtils.substring(null, 1, 3); // NullPointerException!!!

マイナスや文字列長より大きな数字を指定すると java.lang.StringIndexOutOfBoundsException が発生します。

TextUtils.substring("tomato", -1, 3);

クラス

TextUtils.SimpleStringSplitter

文字列を指定の区切り文字で分割するクラスです。

文字列を '-' で区切りたい時は下記のようにインスタンスを取得します。

TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter('-');

この splitter を文字列 "a-b-c" に対して使用すると

splitter.setString("a-b-c");

splitter 内部にて分割した文字列へのイテレータが生成され使用できるようになります。

splitter.setString("a-b-c");
for (;splitter.hasNext();) {
    System.out.println(splitter.next());
}

この出力結果は下記の通りです。

a
b
c

区切り文字がない場合は要素1のイテレータが取得できます。

splitter.setString("9");

注意

setString に null を渡すと NullPointerException が発生します。

splitter.setString(null);

まとめ

Android SDK の android.text.TextUtils について調べてみました。知っておくと無用の再実装を防げる便利なメソッドが結構あるので、自分で StringUtils を作る前にドキュメントとソースコードを眺めておくとよいかもしれません。

参考

今回の検証用コード(Gist)

https://gist.github.com/toastkidjp/7230227dd16b80bd4eea9da652be3637


ほか

Deprecated になっているメソッドは飛ばしたのですが、気になるメソッドが1つあったので以下に書きます。

getReverse(CharSequence source, int start, int end)

面白そうなメソッドですが、 APIレベル24で Deprecated になったようです。
「使うな」とだけ書いてあって理由や代替案がない投げっぷりが何とも言えません。

This method was deprecated in API level 24. Do not use.

    private static class Reverser
    implements CharSequence, GetChars
    {
        public Reverser(CharSequence source, int start, int end) {
            mSource = source;
            mStart = start;
            mEnd = end;
        }

        public int length() {
            return mEnd - mStart;
        }

        public CharSequence subSequence(int start, int end) {
            char[] buf = new char[end - start];

            getChars(start, end, buf, 0);
            return new String(buf);
        }

        @Override
        public String toString() {
            return subSequence(0, length()).toString();
        }

        public char charAt(int off) {
            return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
        }

        @SuppressWarnings("deprecation")
        public void getChars(int start, int end, char[] dest, int destoff) {
            TextUtils.getChars(mSource, start + mStart, end + mStart,
                               dest, destoff);
            AndroidCharacter.mirror(dest, 0, end - start);

            int len = end - start;
            int n = (end - start) / 2;
            for (int i = 0; i < n; i++) {
                char tmp = dest[destoff + i];

                dest[destoff + i] = dest[destoff + len - i - 1];
                dest[destoff + len - i - 1] = tmp;
            }
        }

        private CharSequence mSource;
        private int mStart;
        private int mEnd;
    }