Java8 で StringBuilder/StringBuffer クラスがリファクタリングされていました。

ついに Java SE 8 がリリースされました!
そこで、早速ダウンロードして、Java 8 のソースコード(src.zip)を Java 7と比較してみたところ、公表はされていないのですが、ちょこちょことリファクタリングされていることがわかりました。

そこで、そのうち StringBuilder/StringBuffer クラスについて調べてみました。

引数に null が渡されたときの処理

append メソッドの仕様で、「引数が null の場合、"null" という4文字が追加される」というのがあります。
この仕様について、Java 7までは "null" という文字列を追加するという実装がされていました。

// Java 7 Update 51 (AbstractStringBuilder クラス 422行目〜)
    public AbstractStringBuilder append(StringBuffer sb) {
        if (sb == null)
            return append("null");
        (…中略…)
        return this;
    }


これが、Java 8 では 'n', 'u', 'l', 'l' という4つの文字を追加するという実装に変わりました。

// Java 8 (AbstractStringBuilder クラス 428行目〜)
    public AbstractStringBuilder append(StringBuffer sb) {
        if (sb == null)
            return appendNull();
        (…中略…)
        return this;
    }

    private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }

結果として、元の処理にあった配列のコピー*1がなくなっていました。
ちょっとした変更ですが、これによってマイクロベンチマークで2倍のパフォーマンス向上が確認された(improves it by a factor of 2 on microbenchmarks.)そうです。
Bug ID: JDK-8010849 (str) Optimize StringBuilder.append(null)

引数に StringBuilder が渡されたときの処理

もともと、append(CharSequence s) メソッドの中では、引数で渡されたオブジェクトが StringBuffer だった場合に高速な処理が行われるようになっていました。
(普通は一文字ずつコピーするが、StringBuffer のときは内部の配列を直接コピーする)

// Java 7 Update 51 (AbstractStringBuilder クラス 433行目〜)
    public AbstractStringBuilder append(CharSequence s) {
        (…中略…)
        if (s instanceof StringBuffer)
            return this.append((StringBuffer)s);    // StringBuffer 内の配列をコピー
        return this.append(s, 0, s.length());    // CharSequence から一文字ずつコピー
    }


この型の判定とキャストが、 StringBuffer と StringBuilder の共通の親クラスである AbstractStringBuilder に変更されていました。
これによって、StringBuilder の場合も高速な処理が行われるようになっていました。

// Java 8 (AbstractStringBuilder クラス 453行目〜)
    public AbstractStringBuilder append(CharSequence s) {
        (…中略…)
        if (s instanceof AbstractStringBuilder)
            return this.append((AbstractStringBuilder)s);

        return this.append(s, 0, s.length());
    }

StringBuffer より StringBuilder の方が使用頻度が高いので、このリファクタリングの効果は結構大きそうな気がします。

indexOf, lastIndexOf メソッドの高速化

StringBuilder/StringBuffer の indexOf メソッドの処理は、String クラスの indexOf メソッドに移譲されています。
これまでは、このときに str.toCharArray() メソッドを呼び出して新しい char 配列を生成していました。

// Java 7 Update 51 (AbstractStringBuilder クラス 1288行目〜)
    public int indexOf(String str, int fromIndex) {
        return String.indexOf(value, 0, count,
                              str.toCharArray(), 0, str.length(), fromIndex);
    }


Java 8 では、この char 配列の生成が無くなっていました。

// Java 8 (AbstractStringBuilder クラス 1320行目〜)
    public int indexOf(String str, int fromIndex) {
        return String.indexOf(value, 0, count, str, fromIndex);
    }


Java 7 までは「引数に char 配列をとる indexOf メソッド」しかありませんでしたが、今回新たに String クラスの方に「引数に文字列をとる indexOf メソッド」を追加することで、これを可能にしていました。
これにより、String クラス内の char 配列をそのまま使って処理ができるので、わざわざ新たに char 配列を生成する必要がなくなった、ということのようです。

// Java 8 (String クラス 1720行目〜)
    static int indexOf(char[] source, int sourceOffset, int sourceCount,
            String target, int fromIndex) {
        return indexOf(source, sourceOffset, sourceCount,
                       target.value, 0, target.value.length,
                       fromIndex);
    }

処理のコピペをなくす

AbstractStringBuilder で insert メソッドは、このように実装されています。

// Java 7 Update 51 (AbstractStringBuilder クラス 1036行目〜)
    public AbstractStringBuilder insert(int dstOffset, CharSequence s) {
        if (s == null)
            s = "null";
        if (s instanceof String)
            return this.insert(dstOffset, (String)s);
        return this.insert(dstOffset, s, 0, s.length());
    }

一方で、これを継承した StringBuilder クラスでは、以下のように実装されていました。

// Java 7 Update 51 (StringBuilder クラス 306行目〜)
    public StringBuilder insert(int dstOffset, CharSequence s) {
        if (s == null)
            s = "null";
        if (s instanceof String)
            return this.insert(dstOffset, (String)s);
        return this.insert(dstOffset, s, 0, s.length());
    }

…全く同じです。
たぶん、親クラスで実装するか子クラスで実装するかで混乱してこうなってしまっていたのではないかと思います。


Java 8 では StringBuilder クラスで処理をせずに親クラスである AbstractStringBuilder の実装に任せるように変更されていました。
ちょっとした変更ですが、処理がわかりやすくなったと感じました。

// Java 8 (StringBuilder クラス 308行目〜)
    public StringBuilder insert(int dstOffset, CharSequence s) {
        super.insert(dstOffset, s);
        return this;
    }

その他

そのほかにも、以下のような点がリファクタリングされていました。

  • オーバーライドしたメソッドに @Override を付加
  • StringBuffer クラスに、toString() 用のキャッシュを追加*2
    • 2014年3月24日追記: StringBuffer クラスの変更について、詳細を id:naoya2k さんに教えていただきました。(コメント欄を参照)

感想

Java 7 の時にもリファクタリングは行われていたのですが、(Java7 で String クラスがリファクタリングされていましたJava7 Update6 で String クラスがさらにリファクタリングされていました)今回も案の定リファクタリングされていました。
このような小さな変更は、とても読んでいて面白いですし、どんどん内部が洗練されていっているというのを感じました。


Java 9 ではどんなリファクタリングが行われるんでしょうか。楽しみです。

*1:省略していますが、append("null") の先でそのような処理が行われていました。

*2:このレビューコメントを読んだところ、「Java 5 でパフォーマンスが劣化していたが、実際のコードには影響がないので修正していなかった」とあります。英語力不足で、その先の議論は追えませんでした…。RFR: 8013395 StringBuffer.toString performance regression impacting embedded benchmarks