Flyweightパターン
GoFのデザインパターン本では、ソフトウェア開発でよくある問題についての23パターンの解決策が名前付きで紹介されています。著者の場合も、長年の間にこれらのパターンに何度となくお世話になりました。CommandやTemplate Methodなどのパターンは、今でも高く評価しています。
もっとも、なかにはごく限られた場面でしか使えそうにないパターンもいくつかあります。その一例がFlyweightです。しかし、FlyweightはJava自体が大きく依存しているデザインパターンなのです。
Wikipediaによれば、Flyweightパターンは、「多数のオブジェクトを操作する必要があり、これらのオブジェクトに余計なデータを持たせる余裕がない」場合に適しています。Javaでは、StringオブジェクトがFlyweightパターンで管理されています。Javaの固定Stringリテラルはすべてリテラルプールに格納され、重複するリテラルについてはプール内にコピーが1つだけ保持されます。JUnitで作成した簡単な「言語テスト」を使用してこのことを説明しましょう。
@Test public void pool() { String author = "Henry David Thoreau"; String authorCopy = "Henry David Thoreau"; assertTrue(author.equals(authorCopy)); assertTrue(author == authorCopy); }
2つのStringオブジェクトの内容は同じなので、当然この2つのオブジェクトは意味的に等価です。最初のアサーションでは、equals()
を呼び出すことによって2つのオブジェクトの等価性を確認しています。実は、この2つのオブジェクトはメモリ内でも同じ場所に格納されています。2番目のアサーションでは、author
とauthorCopy
の参照(アドレス)を比較しています。
2つのStringオブジェクトは別々に生成されていますが、Javaは密かにこれらのオブジェクトを同じ場所に格納して、メモリ空間を節約しています。これにより、かなりのメモリ節約効果が期待できます。プロファイリングにより、標準的なアプリケーションで生成されるすべてのオブジェクトのうち、3分の1から2分の1はStringオブジェクトであるということがわかっています。
余談ですが、JavaではこのようにFlyweightパターンを用いてStringの格納領域を最小限に抑えているため、経験の少ないプログラマが大きな失敗をすることがよくあります。2つの参照を比較して、その参照が同じ文字の並びを保持しているかどうかを判定する場合に、Javaプログラミングの初心者は次のようなコーディングをしがちです。
if (author == authorCopy)
前述のテストで示したように、この比較は成立します。この2つのオブジェクトがJavaアプリケーションの実行フロー中の別々のタイミングで生成された場合でも、この比較が真を返すことがあります。しかし残念ながら、Javaでは、動的に生成されたStringはリテラルプールに自動的に格納されません。次のテストでは、最後のアサーションは失敗します。
@Test public void pool() { String author = "Henry David Thoreau"; String authorCopy = "Henry David Thoreau"; assertTrue(author.equals(authorCopy)); assertTrue(author == authorCopy); StringBuilder builder = new StringBuilder("Henry"); builder.append(" David Thoreau"); assertTrue(author.equals(builder.toString())); // this assertion fails! assertTrue(author == builder.toString()); }
Stringリテラル「Henry David Thoreau」はコンパイル時に抽出可能であるため、このリテラルはリテラルプールに存在します。しかし、コンパイルの時点では、このコードを実行した結果として「 David Thoreau」が「Henry」リテラルに連結されることは保証されません。理論上は、コンパイラでこのような例を解決することも可能ですが、実際に行った場合にはコンパイル時間が著しく長くなるおそれがあります。また、このような方法が機能するのは、ごく単純な場合に限られています(動的に生成された文字列を強制的にリテラルプールに格納するには、Stringのintern()
メソッドを使用して新しいStringを生成します)。
そこでベストプラクティスとして、Stringの参照を比較するときは必ずequals()
を使うようにしましょう。