Javaの「二重括弧初期化」の効率は?質問する

Javaの「二重括弧初期化」の効率は?質問する

Javaの隠れた機能一番上の回答は二重括弧の初期化非常に魅力的な構文です:

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

このイディオムは、インスタンス初期化子のみを含む匿名の内部クラスを作成し、「包含スコープ内の任意の [...] メソッドを使用できます」。

主な質問: これは思ったほど非効率的ですか? その使用は 1 回限りの初期化に限定する必要がありますか? (そしてもちろん、見せびらかすことも!)

2 番目の質問: 新しい HashSet は、インスタンス初期化子で使用される「this」である必要があります... 誰かそのメカニズムについて説明できますか?

3 番目の質問: この慣用句は、本番コードで使用するには難解すぎるでしょうか?

要約:非常に素晴らしい回答でした。皆さん、ありがとうございました。質問 (3) では、構文は明確であるべきだと感じた人がいました (ただし、特にコードに精通していない開発者に渡される場合は、時々コメントすることをお勧めします)。

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

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

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

2017年7月: ベルドゥング良い要約がある二重中括弧の初期化はアンチパターンであると考えています。

2017 年 12 月: @Basil Bourque は、新しい Java 9 では次のように実行できると述べています。

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

それは間違いなく正しい方法です。以前のバージョンにこだわっている場合は、Google コレクションの ImmutableSet

ベストアンサー1

匿名内部クラスに夢中になりすぎると、次のような問題が発生します。

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ファイルにコンパイルされます。

すでに述べたように、「二重中括弧の初期化」は、インスタンス初期化ブロックを持つ匿名の内部クラスです。つまり、通常は単一のオブジェクトを作成する目的で、「初期化」ごとに新しいクラスが作成されます。

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

二重中括弧の初期化を使用すると、多少のオーバーヘッドが発生するようですので、あまりやりすぎるのはよくないかもしれません。しかし、Eddie がコメントで指摘しているように、影響を完全に確かめることはできません。


参考までに、二重中括弧の初期化は次のとおりです。

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

これは Java の「隠された」機能のように見えますが、単に次のコードを書き直しただけです。

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要素を含む 1000 個の sを作成し"Hello"、次の 2 つの方法を使用してメソッド"World!"経由でそれらに追加します。add

方法 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);
  }
}

ArrayList1000秒の と 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

ご覧のとおり、二重中括弧の初期化には約 190 ミリ秒の実行時間が顕著にかかります。

一方、ArrayList初期化実行時間は 0 ミリ秒となりました。もちろんタイマーの分解能も考慮する必要がありますが、15 ミリ秒未満になると思われます。

したがって、2 つの方法の実行時間には顕著な違いがあるようです。2 つの初期化方法には確かにいくらかのオーバーヘッドがあるようです。

そして、はい、二重中括弧初期化テスト プログラム.classをコンパイルすることによって 1000 個のファイルが生成されましたTest1

おすすめ記事