The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目5:不必要なオブジェクトの生成を避ける

機能的に同じオブジェクトを、必要になるごとに再生成するのは避ける。

よくない例

極端にだめな例としては以下の様なコードがある。

// だめな例
String s = new String("stringee");

"stringee" 自身が String オブジェクトであり、String コンストラクタで生成されるオブジェクトと同様の機能を持つ。 そのため、これは二重にオブジェクトを生成していることになる。

正しくは以下のようにする。

// よい例
String s = "stinggette"

言語仕様上、同じ内容の文字列リテラルで生成されるインスタンスは再利用することが保証されている(同一仮想マシン上のみ)ため、"stingette" のインスタンスは一度しか生成されずに済み、他の参照に対して再利用される。

項目1の static ファクトリーメソッドを使うと、この再利用性をさらに担保できる。 Boolean.valueOf(String) は内部的には Boolean.TRUE と Boolan.FALSE の2つの static オブジェクトを再利用している1。 一方、Boolean(String) コンストラクタを利用すると、Boolean のインスタンスがその都度生成されてしまう。

// だめな例。毎度インスタンスが生成される。
Boolean b = new Boolean("true")

// よい例。Boolean.TRUE と Boolean.FALSE が再利用される。
Boolean b = Boolean.valueOf("true")

変更されない可変オブジェクト

不変オブジェクトを再利用することは「不必要なオブジェクトの生成を避ける」ために最もよく利用される手法だ。 これに加えて、変更されないと分かっている可変オブジェクトの再利用も検討する。

ベビーブームかどうかを判定するメソッド isBabyBoomer を持つ以下の様な Person クラスがある。

public class Person {
    private final Date birthDate;
    public boolean isBabyBoomer() {
        Calendar gmtCal = Calender.getInstance(TimeZone.getTimeZone("GMT"));
        getCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomStart = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        Date boomEnd = gmtCal.getTime();
        return birthDate.compareTo(boomStart) >= && birthDate.compareTo(boomEnd);
    }
}

このコードの isBabyBoomer() メソッドは Calendar インスタンス、Timezone インスタンス、Date インスタンスを2つ、合計4つのインスタンスを生成している。 しかし、この4つのインスタンスは呼び出しごとに生成する必要はなく、Person クラスに対して1つだけあればよい。

そこで、static イニシャライザを利用して以下の様にする。

public class Person {
    private final Date birthDate;
    
    // ベビーブームの開始と終わり
    private static final Date BOOM_START;
    private static final Date BOOM_END;
    
    static {
        Calendar gmtCal = Calender.getInstance(TimeZone.getTimeZone("GMT"));
        getCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_START = gmtCal.getTime();
        gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
        BOOM_END = gmtCal.getTime();
    }

    public boolean isBabyBoomer() {
        return birthDate.compareTo(boomStart) >= && birthDate.compareTo(boomEnd);
    }
}

改良されたこのバージョンは毎回インスタンスを生成する代わりに、それぞれに対して一度だけインスタンスを生成している。 著者の環境では isBabyBoomer の 1000 万回の呼び出しにおけるパフォーマンスが 32,000 msec から 130 msec に改善した。 それに加えてベビーブームの開始と終わりは不変であることが示されているので、コードの可読性も向上している。

もし、この isBabyBoomer が呼び出されない可能性があれば、この4つのオブジェクトは無駄になる。 そこで、遅延初期化(項目71)を検討することもできるが、遅延初期化はたいていの場合、実装を複雑にし、大したパフォーマンス向上にもならないので、おすすめはしない。

自動ボクシング

Java リリース 1.5 では、不必要なオブジェクトを生成する新たな方法ができたので注意する。

それは自動ボクシング。自動ボクシングによって、基本データ型と対応するラッパークラスを混在させることができるようになった。

Integer a = 12345; // 自動ボクシング
int b = a; // 自動アンボクシング

これは、単に型変換が自動的に行われるようになっただけで、これら二つの型の違いは取り除かれていない。

特にこれらはパフォーマンスに大きな影響を与えることがあるので注意が必要だ(項目49)。

public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += 1
    }
    System.out.println(sum);
}

このプログラムでは sum が Long 型で、それをインクリメントする i は long 型だ。 その結果、毎回自動ボクシングが起こり 231 個の不必要な Long インスタンスを生成している。

sum を long 型に変えたところ著者の環境ではパフォーマンス 43.2 sec が 6.8 sec に改善した。

なるべく、基本データ型を選択するようにし、意図しない自動ボクシングに注意すること。

温度感

本項目は、オブジェクト生成はコストがかかるという主張をしているが、常に避けるべきだという意味ではない。 明瞭性や簡潔性のために小さなオブジェクトを生成するのはかまわない。最新の JVM 実装であれば特段の処理が行われないオブジェクトの生成はほぼゼロである。

逆に、自分でオブジェクトプールを保持する方法は一般的には悪い方法である。もちろん、データベースコネクションなど、一般的に知られた重いオブジェクトを対象とする場合には正当化される。

一方、本項目と対照的なのが防御的コピー(項目39)。 防御的コピーが必要な場合は、常にオブジェクトを生成するべき。 不必要に重複したオブジェクトの生成は、悪質なバグやセキュリティホールに繋がる。

不必要にオブジェクトが生成された場合は、単にコードスタイルとパフォーマンスに影響するだけ。

感想

オブジェクトを static 化で一度だけ生成するようにするのは、パフォーマンス向上が目的というよりも、設計意図とかオブジェクトが持つライフサイクルの反映という側面の方が大事だと思う。

ライフサイクルが反映されていないコードはほんと読みづらいし、理解しづらい。

「うーん、概念的にこのオブジェクトって一度だけ生成すればいいんだよなぁ、、、」⇒(コードを一通り見る)⇒「大丈夫そうだから、一回だけ生成するように変えとくか」⇒(動かない)⇒「うわ、ここで引数で渡す時 const ついてなくて、メンバの参照先の配列が変更されるじゃん/(^o^)\」みたいなのを過去のプロジェクトで経験した。

「(コードを一通り見る)」のところで気づかない俺が悪いんだけど…

(c) The King's Museum