1. ホーム

[解決済み】Javaの「ダブルブレース初期化」の効率化?

2022-03-23 22:23:41

質問

Java の隠れた機能 を挙げています。 ダブルブレースの初期化 を使用すると 非常に 魅力的な構文です。

Set<String> flavors = new HashSet<String>() {{
    add("vanilla");
    add("strawberry");
    add("chocolate");
    add("butter pecan");
}};

このイディオムは、インスタンス・イニシャライザだけを持つ無名の内部クラスを作成し、そのクラスは "含むスコープで任意の [...] メソッドを使用できます" 。

本題です。これは 非効率 という感じでしょうか? 一回限りの初期化に限定すべきなのでしょうか? (もちろん、見せびらかしも!)。

2つ目の質問です。新しいHashSetは、インスタンスのイニシャライザーで使用される"this"でなければなりません...誰かこのメカニズムについて教えてください。

3つ目の質問です。このイディオムも 不明瞭 プロダクションコードで使うには?

概要 とても素晴らしい回答です、皆さんありがとうございます。質問(3)では、構文が明確であるべきだと感じているようです(ただし、特にコードが馴染みのない開発者に渡る場合は、時折コメントすることをお勧めします)。

質問(1)について、生成されたコードは素早く実行されるはずです。余分な .class ファイルは jar ファイルの散乱の原因となり、プログラムの起動を若干遅くします (測定してくれた @coobird に感謝します)。ガベージコレクションが影響を受ける可能性があると @Thilo が指摘しており、余分にロードされたクラスのメモリコストが要因になる場合もあります。

質問(2)は、私にとって最も興味深いものになりました。回答を理解すると、DBIで起こっていることは、匿名の内部クラスがnew演算子によって構築されるオブジェクトのクラスを拡張し、したがって構築されるインスタンスを参照する"this"値を持っているということです。非常に巧妙です。

全体として、DBIは知的好奇心の塊のようなものだと思います。 Coobirdや他の人は、Arrays.asList、varargsメソッド、Google Collections、そして提案されているJava 7 Collection literalsで同じ効果を得られると指摘しています。 Scala、JRuby、Groovyのような新しいJVM言語も、リスト構築のための簡潔な記法を提供し、Javaとうまく相互運用できます。 DBIはクラスパスを乱し、クラスの読み込みを少し遅くし、コードを少しわかりにくくすることを考えると、おそらく敬遠されることでしょう。しかし、私は、SCJPを取得したばかりで、Javaのセマンティクスについての気さくな議論が好きな友人に、これを勧めるつもりです!;-) みなさん、ありがとうございます。

7/2017: ベアード は、良いまとめがあります。 の二重ブレース初期化について、アンチパターンと考えています。

12/2017: @Basil Bourqueは、新しいJava 9では、あなたが言うことができることに注意してください。

Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");

確かにその通りですね。 もし、以前のバージョンにこだわっているのなら、次のものを見てください。 Google CollectionsのImmutableSet .

解決するには?

ここで、匿名インナークラスで調子に乗りすぎると、こんな問題が発生します。

2009/05/27  16:35             1,602 DemoApp2$1.class
2009/05/27  16:35             1,976 DemoApp2$10.class
2009/05/27  16:35             1,919 DemoApp2$11.class
2009/05/27  16:35             2,404 DemoApp2$12.class
2009/05/27  16:35             1,197 DemoApp2$13.class

/* snip */

2009/05/27  16:35             1,953 DemoApp2$30.class
2009/05/27  16:35             1,910 DemoApp2$31.class
2009/05/27  16:35             2,007 DemoApp2$32.class
2009/05/27  16:35               926 DemoApp2$33$1$1.class
2009/05/27  16:35             4,104 DemoApp2$33$1.class
2009/05/27  16:35             2,849 DemoApp2$33.class
2009/05/27  16:35               926 DemoApp2$34$1$1.class
2009/05/27  16:35             4,234 DemoApp2$34$1.class
2009/05/27  16:35             2,849 DemoApp2$34.class

/* snip */

2009/05/27  16:35               614 DemoApp2$40.class
2009/05/27  16:35             2,344 DemoApp2$5.class
2009/05/27  16:35             1,551 DemoApp2$6.class
2009/05/27  16:35             1,604 DemoApp2$7.class
2009/05/27  16:35             1,809 DemoApp2$8.class
2009/05/27  16:35             2,022 DemoApp2$9.class

これらはすべて、シンプルなアプリケーションを作っているときに生成されたクラスで、大量の無名インナークラスを使っています -- 各クラスは別々の class ファイルを作成します。

このクラスはインスタンス初期化ブロックを持つ無名の内部クラスで、通常1つのオブジェクトを作成するために、初期化のたびに新しいクラスが作成されます。

Java 仮想マシンがこれらのクラスを使用する際にすべて読み込む必要があることを考慮すると、そのために バイトコード検証 の処理などです。をすべて保存するために必要なディスク容量が増加することは言うまでもありません。 class ファイルを作成します。

ダブルブレイスの初期化を利用すると、少しオーバーヘッドになるような気がするので、あまりやり過ぎない方がいいかもしれませんね。しかし、Eddieがコメントで述べているように、その影響を完全に確認することは不可能です。


参考までに、ダブルブレースの初期化は以下の通りです。

List<String> list = new ArrayList<String>() {{
    add("Hello");
    add("World!");
}};

Java の "hidden" 機能のように見えますが、単に書き換えただけです。

List<String> list = new ArrayList<String>() {

    // Instance initialization block
    {
        add("Hello");
        add("World!");
    }
};

つまり、基本的には インスタンス初期化ブロック の一部であり 匿名内部クラス .


ジョシュア・ブロッホの コレクション リテラルの提案 に対して プロジェクトコイン は、そのような内容でした。

List<Integer> intList = [1, 2, 3, 4];

Set<String> strSet = {"Apple", "Banana", "Cactus"};

Map<String, Integer> truthMap = { "answer" : 42 };

悲しいかな、それは を実現できませんでした。 は、Java 7にも8にも搭載されず、無期限で棚上げされた。


実験

私が試した簡単な実験は次のとおりです。 ArrayList という要素で "Hello""World!" を経由して追加されます。 add という2つのメソッドを使用しています。

方法1:ダブルブレイスの初期化

List<String> l = new ArrayList<String>() {{
  add("Hello");
  add("World!");
}};

方法2: ArrayListadd

List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");

2つの方法で1000回の初期化を行うために、Javaのソースファイルを書き出す簡単なプログラムを作成しました。

テスト1:

class Test1 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    List<String> l1 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    /* snip */

    List<String> l999 = new ArrayList<String>() {{
      add("Hello");
      add("World!");
    }};

    System.out.println(System.currentTimeMillis() - st);
  }
}

テスト2

class Test2 {
  public static void main(String[] s) {
    long st = System.currentTimeMillis();

    List<String> l0 = new ArrayList<String>();
    l0.add("Hello");
    l0.add("World!");

    List<String> l1 = new ArrayList<String>();
    l1.add("Hello");
    l1.add("World!");

    /* snip */

    List<String> l999 = new ArrayList<String>();
    l999.add("Hello");
    l999.add("World!");

    System.out.println(System.currentTimeMillis() - st);
  }
}

を初期化するまでの経過時間は、1000秒に1回であることに注意してください。 ArrayList を拡張する1000個の匿名インナークラスと ArrayList を使用してチェックします。 System.currentTimeMillis そのため、タイマーの分解能はあまり高くありません。私のWindowsシステムでは、解像度は15-16ミリ秒程度です。

2つのテストを10回実行した結果は、以下の通りです。

Test1 Times (ms)           Test2 Times (ms)
----------------           ----------------
           187                          0
           203                          0
           203                          0
           188                          0
           188                          0
           187                          0
           203                          0
           188                          0
           188                          0
           203                          0

見ての通り、ダブルブレースの初期化の実行時間が約190msと顕著に現れています。

一方 ArrayList 初期化実行時間は0msと出ました。もちろん、タイマーの分解能を考慮する必要がありますが、15ms以下であることは間違いないと思われます。

ということで、2つの方法の実行時間には顕著な差があるようです。確かに、2つの初期化メソッドにはオーバーヘッドがあるように見えます。

そして、そう、1000個の .class をコンパイルして生成されたファイルです。 Test1 ダブルブレース初期化テストプログラム