[intlink id=”133″](前回)の最後に『しかし、まだ改良する余地はあります。ヒントは「メソッドtoString(Collection)がどのように使われるのか?」です。』と謎を残しておきました。今回はその謎解きです。性能に注目して前回のプログラムを見てください。

前回のプログラム
import java.util.Iterator;
import java.util.Collection;
public class CollectionUtility
{
    public static String toString(Collection aCollection)
    {
        String tString = "";
        Iterator tIterator = aCollection.iterator();
        if(tIterator.hasNext())
        {
            tString += tIterator.next();
            while(tIterator.hasNext())
                tString += ", " + tIterator.next();
        }
        return tString;
    }
}

そのプログラムが生成するインスタンスの数は?

前回のプログラムは初心者が陥りがちな罠「文字列結合によるインスタンスの大量生成」にはまっています。具体的には、引数aCollectionの要素数だけクラスStringBuilderのインスタンスを生成します。コンパイラが文字列に対する演算子+=を次のように置換してコンパイルするからです。

置換前
B0 += B1 + ... + Bn;
置換後(JDK1.5より前はStringBuilderの代わりにStringBufferが生成される)
B0 = new StringBuffer(B0).append(B1)....append(Bn).toString();

実行環境にもよりますが、一般にインスタンス生成にはかなり計算時間がかかります。インスタンスを配置するために空メモリを検索したり作ったりするからです。そのため、大量のインスタンス生成は避けるべきです。

参考までに、各種手続の計算時間の比率を次の表にまとめました。この表の値はJDK1.6.0のVirual Machineの最適化オプションを全てoffにして計測しました。

手続 時間比率
インスタンス生成 134.6
インスタンスメソッド呼出 35.3
クラスメソッド呼出 28.3
インスタンスフィールドアクセス 7.8
クラスフィールドアクセス 8.2
ローカル変数アクセス 1.0

それでは、この状況を改善するにはどうすればよいのでしょうか?答えは簡単です。コンパイラが行っている置換を次のように発展させればよいだけです。こうすることで、クラスStringBuilderのインスタンスの生成を1回にすることができます。

プログラム(StringBuilder版)
import java.util.Iterator;
import java.util.Collection;
public class CollectionUtility
{
    public static String toString(Collection aCollection)
    {
        StringBuilder tStringBuilder = new StringBuilder();
        Iterator tIterator = aCollection.iterator();
        if(tIterator.hasNext())
        {
            tStringBuilder.append(tIterator.next());
            while(tIterator.hasNext())
                tStringBuilder.append(", ").append(tIterator.next());
        }
        return tStringBuilder.toString();
    }
}

前の修正でかなり性能が改善されました。しかし、まだまだ性能を改善する余地があります。

メソッドtoString(Collection)がどのような状況で呼び出されるのか想像してください。

何かのメッセージを生成しなければいけないときに、このメソッドが繰り返し呼び出される状況が容易に想像できませんか?例えば、デバッグのためにフィールドの内容を表示する状況が典型的です。そのような状況では、呼び出された回数だけクラスStringBuilderのインスタンスが生成されます。さらに悪いことにメソッドが返す文字列を結合するために大量のインスタンス生成や文字列転送が発生します。

この状況を改善するにはどうすればよいでしょう?答えは簡単です。次のようにインスタンスを生成しないバージョンのメソッドtoString(Collection, StringBuilder)を用意するだけです。この修正はメソッド抽出(Eclipseなどで実現されているリファクタリング機能)で簡単に行えます。こうしておけば、メソッドtoString(Collection, StringBuilder)の呼出毎にクラスStringBuilderのインスタンスを生成する必要はありませんし、呼出毎に追記してくれるので文字列転送も起こりません。

import java.util.Iterator;
import java.util.Collection;
public class CollectionUtility
{
    public static String toString(Collection aCollection)
    {
        StringBuilder tStringBuilder = new StringBuilder();
        toString(aCollection, tStringBuilder);
        return tStringBuilder.toString();
    }
    public static void toString(Collection aCollection, StringBuilder aStringBuilder)
    {
        Iterator tIterator = aCollection.iterator();
        if(tIterator.hasNext())
        {
            aStringBuilder.append(tIterator.next());
            while(tIterator.hasNext())
                aStringBuilder.append(", ").append(tIterator.next());
        }
    }
}

以上のように、メソッドが使われる状況を想定するとよい設計を見出せる可能性が高まるのでお勧めです。

今回はかなり性能改善ができました。しかし、これで安心してはいけません。まだまだ性能は改善できます。並行プログラミングで使われる場合を想像するとよいでしょう。

カテゴリー: 技術情報

5件のコメント

koreyasu · 2007-06-20 23:26

インデントしときました。

ojarz · 2007-06-21 11:14

うむ。
予測した結果だった。

yamaguchi · 2007-06-21 11:27

簡単すぎたか。> ojarz
今度もあたればすごい。

koreyasu · 2007-06-21 17:59

個人的には

if(tIterator.hasNext())
{
  aStringBuilder.append(tIterator.next());
  while(tIterator.hasNext())
    aStringBuilder.append(“, “).append(tIterator.next());
}

の部分は、

if(!tIterator.hasNext())
  return;

aStringBuilder.append(tIterator.next());
while(tIterator.hasNext())
  aStringBuilder.append(“, “).append(tIterator.next());

のが好きかな。ネストは少ない方が良いって考え。
ちなみに、1行でも{}は付けたいところ。

squld · 2007-06-22 00:11

外からフォーマットを与えるって拡張かと思ってたのに外れた・・・orz

現在コメントは受け付けていません。