文字列の連結

Javaで文字列の連結の方法にはいくつかある。まずはそれらの方法をまとめ、最後に新しい方法を考えてみる。

+演算子

一番直感的なのは、+演算子による連結だろう。しかし、これはコンパイル時定数同士の連結でない限り最も効率が悪いので、使うべきではない。ただし、コンパイル時定数同士の連結では最も効率がよい*1ため、以下のようなコードは問題ない。

public static final String LN = "\n";
public static final int HOGE_NUM = 100;

public String func() {
    return HOGE_NUM + LN;
}

この場合、コンパイル時に文字列が結合され、"100\n"となる。このように、文字列定数同士の結合だけでなく、文字列定数と他のプリミティブ型のコンパイル時定数の連結の場合もコンパイル時に連結される。
これ以外の連結では、+演算子を使用するよりも効率のいい方法が存在する。

String#concatメソッド

あまり知られていない方法(?)では、String#concatメソッドを使うものもある。この方法は+演算子を使用する方法よりも効率的ではあるものの、連続した連結には向かない。そのため、concatメソッドを使った方法を使用したほうがいい場面というのはかなり限られる。後で説明するStringBuilderを使用する方法ではStringBuilderのインスタンスを生成しなければならないが、その生成コストが連結のコストよりも高くなる場合にのみこの方法を使用するといい。

public String getAnnotationStr(String annotationName) {
    return "@".concat(str);
}

また、concatメソッドはインスタンスメソッドなので、文字列インスタンスがnullでないことが保障されない場合は、nullに対するチェックが必要になる*2

StringBuilder(StringBuffer)

一番実用性の高い方法はStringBuilder(もしくはStringBuffer 以下、StringBuilderとあったらStringBufferも同様のことが当てはまる)クラスを使用したものだろう。この方法は文字列の連続連結を行う場合にはもっとも効率がよく、文字列以外の連結も可能なので汎用性が高い。

public String func(Object[] objs, int expectedMaxSize) {
    StringBuilder result = new StringBuilder(expectedMaxSize);
    for (Object obj : objs)
        result.append(obj);
    return String.valueOf(result);  // ここではresult.toString()でも問題はない
}

StringBuilderは内部にバッファを持っており、このバッファ以上の文字列の連結を行おうとすると、自動でバッファが拡張される。しかし、バッファの拡張にはコストがかかるので、可能であればコンストラクタに結合後の文字列長の最大値を指定しておくと、はじめからバッファがその分確保されるのでバッファの拡張が発生せず、効率がよくなる。
それと、StringBuilderにはcharを引数にとるコンストラクタはないので注意が必要。暗黙裡にintにキャストされ、おそらく想定していた動作をしてくれない。

ユーティリティクラスを作る

最後に、文字列連結のためのユーティリティクラスを作ってみる。内部ではStringBuilderを使用するが、文字列連結のために可変長引数をとるjoinメソッドを使っているので、appendと何回もタイプしなくてもいい。また、バッファ長と初期文字列を同時に指定できるコンストラクタを持つのも特長の一つである。

// 基本的にnullセーフ
public final class StringJointUtil {

    private String baseStr;
    private int bufferLength;
    
    public StringJointUtil(String baseStr, int length) {
        this.baseStr = baseStr;
        if (length < 1)
            length = 1;
        if (baseStr == null)
            bufferLength = length;
        else
            // バッファ長はベースとなる文字列の長さに、引数lengthを足したもの
            // つまり、引数lengthはベースとなる文字列の長さを考慮に入れる必要がない
            bufferLength = baseStr.length() + length;
    }
    
    public StringJointUtil(String baseStr) {
        this.baseStr        = baseStr;
        this.bufferLength   = 0;
    }
    
    public int getBufferLength() {
        return this.bufferLength;
    }
    
    public void setBufferLength(int length) {
        this.bufferLength = length < 1 ? 1 : length;    // バッファ長は最低でも1
    }
    
    public void addBufferLength(int length) {
        this.bufferLength += length < 1 ? 1 : length;   // 最低でも1足される
    }
    
    // このメソッドで文字列を連結する(実はこのメソッドにObject[]でnullを渡すと例外が発生する)
    public String joint(Object... objects) {
        StringBuilder result = new StringBuilder(bufferLength);
        if (baseStr != null)
            result.append(baseStr);     // ベースとなる文字列がnullの場合は連結しない
        for (Object obj : objects)
            result.append(obj);         // それ以外のときにnullなら連結する
        baseStr = String.valueOf(result);
        return baseStr;
    }
    
    @Override
    public String toString() {
        return String.valueOf(baseStr);
    }
    
    @Override
    public int hashCode() {
        // Eclipseの自動生成機能によって生成
        final int PRIME = 31;
        int result = 1;
        result = PRIME * result + ((baseStr == null) ? 0 : baseStr.hashCode());
        return result;
    }
    
    @Override
    public boolean equals(Object obj) {
        // Eclipseの自動生成機能によって生成
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final StringJointUtil other = (StringJointUtil) obj;
        if (baseStr == null) {
            if (other.baseStr != null)
                return false;
        } else if (!baseStr.equals(other.baseStr))
            return false;
        return true;
    }
}

このクラスを使用すると、SQL文の構築等でStringBuilderよりも可読性が向上する。

public String createSql(String table, String usr, String pass) {
    StringJointUtil joint = new StringJointUtil(
        "select * from ", table.length() + usr.length() + pass.length() + 20);
    return joint.joint(table, " where usr=", usr, "and pass=", pass);
}

これをStringBuilderを用いると、次のようになる。

public String createSql(String table, String usr, String pass) {
    StringBuilder buf = new StringBuilder(
        table.length() + usr.length() + pass.length() + 34);
    buf.append("select * from ").append(table);
    buf.append(" where usr=").append(usr).append("and pass=").append(pass);
    return String.valueOf(buf);
}

ただし、もちろんStringJointUtilが万能というわけではない。StringJointUtilはjointメソッド内でStringBuilderオブジェクトを生成しているので、ループが必要な場合はStringBuilderを直接使ったほうが効率がよくなるし、可読性も変わらない*3
要は、どんな選択肢があるか、またそれぞれの方法の利点や欠点を知る必要があるという分かりきった結論に達するわけである。いいたかったことは実はこっち。


9日修正・・・プログラムがStringJoinUtilになっていたのでStringJointUtilに修正。といっても、意味はほとんど変わらない・・・

*1:というかコンパイル時に連結される

*2:上の例では文字列リテラルでメソッドを起動しているのでチェックは不要

*3:むしろこの用途では標準ライブラリであるStringBuilderを使用したほうが分かりやすくなる