Javaの「ダブルブレース初期化」の効率?




performance collections (12)

私はループを使用して、asList(要素)から暗黙リストをすぐに投げるのではなく、ループを使用する点を除いて、2番目のNatの答え:

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }

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で何が起きているのかは、匿名の内部クラスが新しい演算子によって構築されているオブジェクトのクラスを拡張するため、構築されているインスタンスを参照する "this"値があるということです。 非常にきれい。

全体として、DBIは知的好奇心の一部として私を襲ってきます。 Coobirdらは、Arrays.asList、varargsメソッド、Google Collections、提案されたJava 7 Collectionリテラルで同じ効果を達成できることを指摘しています。 Scala、JRuby、Groovyなどの新しいJVM言語は、リスト構築のための簡潔な表記法を提供し、Javaとうまく相互運用できます。 DBIがクラスパスを混乱させ、クラスの読み込みを少し遅くし、コードをもっと不明瞭にしているとすれば、おそらく私はそれを恥ずかしがるでしょう。 しかし、私はちょうど彼のSCJPを手に入れた友人にこれを春にして、Javaのセマンティクスについての良い気持ちを愛しています! ;-) みんな、ありがとう!

7/2017:Baeldung 二重ブレースの初期化の良い要約持ち、それを反パターンとみなします。

12/2017:@Basil Bourqueは、新しいJava 9では次のように言います。

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

それは確かに行く方法です。 以前のバージョンでは、 Google CollectionsのImmutableSetをご覧ください


多くのクラスを読み込むと、開始まで数ミリ秒を追加できます。 スタートアップがそれほど重要ではなく、スタートアップ後にクラスの効率を見ても違いはありません。

package vanilla.java.perfeg.doublebracket;

import java.util.*;

/**
 * @author plawrey
 */
public class DoubleBracketMain {
    public static void main(String... args) {
        final List<String> list1 = new ArrayList<String>() {
            {
                add("Hello");
                add("World");
                add("!!!");
            }
        };
        List<String> list2 = new ArrayList<String>(list1);
        Set<String> set1 = new LinkedHashSet<String>() {
            {
                addAll(list1);
            }
        };
        Set<String> set2 = new LinkedHashSet<String>();
        set2.addAll(list1);
        Map<Integer, String> map1 = new LinkedHashMap<Integer, String>() {
            {
                put(1, "one");
                put(2, "two");
                put(3, "three");
            }
        };
        Map<Integer, String> map2 = new LinkedHashMap<Integer, String>();
        map2.putAll(map1);

        for (int i = 0; i < 10; i++) {
            long dbTimes = timeComparison(list1, list1)
                    + timeComparison(set1, set1)
                    + timeComparison(map1.keySet(), map1.keySet())
                    + timeComparison(map1.values(), map1.values());
            long times = timeComparison(list2, list2)
                    + timeComparison(set2, set2)
                    + timeComparison(map2.keySet(), map2.keySet())
                    + timeComparison(map2.values(), map2.values());
            if (i > 0)
                System.out.printf("double braced collections took %,d ns and plain collections took %,d ns%n", dbTimes, times);
        }
    }

    public static long timeComparison(Collection a, Collection b) {
        long start = System.nanoTime();
        int runs = 10000000;
        for (int i = 0; i < runs; i++)
            compareCollections(a, b);
        long rate = (System.nanoTime() - start) / runs;
        return rate;
    }

    public static void compareCollections(Collection a, Collection b) {
        if (!a.equals(b) && a.hashCode() != b.hashCode() && !a.toString().equals(b.toString()))
            throw new AssertionError();
    }
}

プリント

double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 34 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns
double braced collections took 36 ns and plain collections took 36 ns

私はこれを研究して、有効な答えによって提供されたものより深いテストをすることに決めました。

コードは次のとおりです: https://gist.github.com/4368924 : https://gist.github.com/4368924

これは私の結論です

私は、ほとんどのランテストでは、内部起動が実際には速かった(いくつかのケースではほぼ2倍)ということに驚きました。 大きな数字で作業するとき、利益は消え去っているようです。

興味深いことに、ループ上に3つのオブジェクトを作成するケースは、他のケースより早く利益が失われます。 なぜこのようなことが起こっているのか分かりませんし、結論を出すためにはもっと多くのテストをすべきです。 具体的な実装を作成すると、クラス定義が再ロードされないようにすることができます(これが起こっている場合)

しかし、大量の場合であっても、単一項目の建物ではほとんどの場合、それほどオーバーヘッドは見られませんでした。

1つのセットバックは、ダブルブレース開始のそれぞれが、ディスクブロック全体をアプリケーションのサイズ(または圧縮時に約1k)に追加する新しいクラスファイルを作成するという事実です。 小さなフットプリントですが、多くの場所で使用されている場合は、影響を受ける可能性があります。 これを1000回使用すると、潜在的にアプリケーション全体にMiBを追加することになります。これは組み込み環境に関係する可能性があります。

私の結論? それは虐待されていない限り使用することができます。

どう考えているか教えてください :)


これまで指摘されていないこのアプローチの1つの特性は、内部クラスを作成するため、そのクラスを含む全体がそのスコープ内に取り込まれることです。 これは、あなたのSetが生きている間は、それを含むインスタンス( this$0 )へのポインタを保持し、それがガベージコレクションされないようにすることを意味します。

これと、新しいクラスが最初の場所で作成されるという事実は、たとえ正規のHashSetがうまく動く(またはより良い)場合でも、(私が本当に構文的な砂糖を必要としているにもかかわらず)この構造体を使いたくないのです。

2番目の質問:新しいHashSetはインスタンスイニシャライザで使用される "this"でなければなりません...誰でも機構を軽視できますか? 私は純粋に "これ"が "味"を初期化するオブジェクトを参照すると期待していたでしょう。

これはインナークラスの仕組みです。 彼らは自分自身でthisを取得しますが、親インスタンスへのポインタも持っています。そのため、オブジェクトも含めてメソッドを呼び出すことができます。 命名の競合が発生した場合、内部クラス(あなたの場合はHashSet)が優先されますが、 "this"の前にクラス名を付けると、外部メソッドも取得できます。

public class Test {

    public void add(Object o) {
    }

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // HashSet
              Test.this.add("hello"); // outer instance 
            }
        };
    }
}

作成されている匿名サブクラスを明確にするために、そこにメソッドを定義することもできます。 たとえば、 HashSet.add()オーバーライドします。

    public Set<String> makeSet() {
        return new HashSet<String>() {
            {
              add("hello"); // not HashSet anymore ...
            }

            @Override
            boolean add(String s){

            }

        };
    }

私が匿名の内部クラスであまりにも疎遠になったときの問題は次のとおりです。

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でもなく、無期限に棚上げされました。

実験

ここでは、私がテストした簡単な実験があります。 "Hello""World!"要素を持つ1000のArrayList作成し"World!" addメソッドを使用して、次の2つのメソッドを使用してそれらに追加されます。

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

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

方法2: ArrayListをインスタンス化し、

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

私は、Javaソースファイルを書き出すためのシンプルなプログラムを作成し、2つのメソッドを使って1000の初期化を行いました。

試験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);
  }
}

ArrayListを初期化するための経過時間と、 ArrayList拡張している1000の匿名の内部クラスは、 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つの初期化メソッドにはオーバーヘッドがあるように見えます。

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


漏れやすい

パフォーマンスへの影響には、ディスク操作+解凍(jar)、クラス検証、パーマネントスペース(Sun's Hotspot JVM)などがあります。 しかし、最悪の場合:それはリークしやすいです。 あなたは単に戻ることはできません。

Set<String> getFlavors(){
  return Collections.unmodifiableSet(flavors)
}

したがって、セットが別のクラスローダーによってロードされた他の部分にエスケープされ、そこに参照が保持されていると、クラス+クラスローダーのツリー全体がリークします。 これを避けるには、HashMapへのコピーが必要です。 new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}) 。 、自分自身ではなく、 new LinkedHashSet(Arrays.asList("xxx","YYY"));


次のテストクラスを取得する:

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

クラスファイルを逆コンパイルすると、次のように表示されます。

public class Test {
  public void test() {
    java.util.Set flavors = new HashSet() {

      final Test this$0;

      {
        this$0 = Test.this;
        super();
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
      }
    };
  }
}

これは私にとって非常に非効率的に見えません。 このようなことでパフォーマンスが心配だったら、私はそれをプロファイルします。 そして、あなたの質問#2は上記のコードによって答えられます:内部クラスの暗黙のコンストラクタ(およびインスタンス初期化子)の中にいるので、 " this "はこの内部クラスを参照します。

はい、この構文はあいまいですが、コメントではあいまいな構文の使用を明確にすることができます。 構文を明確にするために、ほとんどの人は静的初期化子ブロック(JLS 8.7静的初期化子)に精通しています。

public class Sample1 {
    private static final String someVar;
    static {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

また、コンストラクターの使用法(JLS 8.6 Instance Initializers)にも同様の構文(「 static 」という言葉なし)を使用することはできますが、これは実動コードでは使用されていません。 これはあまり知られていません。

public class Sample2 {
    private final String someVar;

    // This is an instance initializer
    {
        String temp = null;
        ..... // block of code setting temp
        someVar = temp;
    }
}

デフォルトのコンストラクタがない場合、 {}間のコードブロックはコンパイラによってコンストラクタに変換されます。 これを念頭に置いて、二重ブレースコードを解きます:

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

最も内側のブレース間のコードブロックは、コンパイラによってコンストラクタに変換されます。 外側の中括弧は、匿名の内部クラスを区切ります。 これを匿名以外のものにする最後のステップを取る:

public void test() {
  Set<String> flavors = new MyHashSet();
}

class MyHashSet extends HashSet<String>() {
    public MyHashSet() {
        add("vanilla");
        add("strawberry");
        add("chocolate");
        add("butter pecan");
    }
}

初期化の目的のためには、オーバーヘッドがまったくない(または無視されるほど小さい)と言いたい。 しかし、 flavors使用はすべてHashSetに対してではなく、 MyHashSetに対してMyHashSetます。 これにはおそらくごくわずかな(おそらく無視できる)オーバーヘッドがあります。 しかし、もう一度、私はそれを心配する前に、私はそれをプロファイルします。

上記のコードは、二重ブレースの初期化と論理的で明示的に等価であり、 " this "が参照する箇所を明確にしていますHashSetを拡張する内部クラス。

インスタンス初期化子の詳細について質問がある場合は、 JLSドキュメントの詳細を確認してください。


二重ブレースの初期化は、メモリリークやその他の問題を引き起こす可能性のあるハックです

この「トリック」を使用する正当な理由はありません。Guavaは、静的ファクトリとビルダーの両方を含む素敵な不変なコレクションを提供しています。コレクションがきれいで、読みやすく、安全な構文で宣言されているところにコレクションを埋め込むことができます。

問題の例は次のようになります。

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

これは短くて読みやすいだけでなく、他の回答に記載されている二重ブレースパターンの多くの問題を回避します。確かに、それは直接構築されたものと同様に動作しますが、HashMap危険でエラーが発生しやすく、より良いオプションがあります。

二重ブレースの初期化を検討しているときはいつでも、APIを再検討するか、構文上のトリックを利用するのではなく、問題に適切に対処するために新しい APIを導入する必要があります。

Error-Proneこのアンチパターンにフラグを立てるようになりました


セットを作成するには、二重ブレースの初期化ではなく、varargsファクトリメソッドを使用できます。

public static Set<T> setOf(T ... elements) {
    return new HashSet<T>(Arrays.asList(elements));
}

Google Collectionsライブラリには、このような便利な方法だけでなく、その他の便利な機能が満載されています。

イディオムのあいまいさについては、私はそれに遭遇し、常に生産コードでそれを使用します。 私は、生産コードを書くことが許されているイディオムによって混乱するプログラマーをもっと心配しています。


この構文は便利ですが、これらのネストされたものとして多くのこの$ 0参照を追加し、それぞれにブレークポイントが設定されていない限り、イニシャライザにデバッグをステップするのが難しい場合があります。 この理由から、私は、定数、特に匿名サブクラスが問題ではない場所(シリアル化が関係しないような)に設定されているバナーセッターに対してのみこれを使用することをお勧めします。


効率はさておき、単体テスト以外の宣言的なコレクションの作成を望んでいることはめったにありません。 私は二重ブレース構文が非常に読みやすいと信じています。

リストの宣言的な構造を具体的に達成する別の方法はArrays.asList(T ...)ように使用することです:

List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");

もちろん、このアプローチの限界は、生成される特定のタイプのリストを制御できないことです。


誰かが二重ブレースの初期化を使うたびに、子猫が殺されます。

構文がかなり珍しく、実際には慣用ではないことは言うまでもなく(味は議論の余地があります)、アプリケーションでは2つの重大な問題が不必要に発生しています。

あなたはあまりにも多くの匿名のクラスを作成しています

二重ブレースの初期化を使用するたびに、新しいクラスが作成されます。 例:

Map source = new HashMap(){{
    put("firstName", "John");
    put("lastName", "Smith");
    put("organizations", new HashMap(){{
        put("0", new HashMap(){{
            put("id", "1234");
        }});
        put("abc", new HashMap(){{
            put("id", "5678");
        }});
    }});
}};

...これらのクラスを生成します:

Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class

それはあなたのクラスローダーのためのかなりのオーバーヘッドです - 何のためにも! もちろん、一度やると初期化に時間がかかりません。 しかし、このアプリケーションを企業アプリケーション全体で20,000回実行すると、すべてのヒープメモリが「構文砂糖」のビットだけを占めますか?

2.潜在的にメモリリークを引き起こしている!

上記のコードを取得してメソッドからそのマップを返すと、そのメソッドの呼び出し元は、無意識のうちにガベージコレクションできない非常に重いリソースを保持している可能性があります。 次の例を考えてみましょう。

public class ReallyHeavyObject {

    // Just to illustrate...
    private int[] tonsOfValues;
    private Resource[] tonsOfResources;

    // This method almost does nothing
    public Map quickHarmlessMethod() {
        Map source = new HashMap(){{
            put("firstName", "John");
            put("lastName", "Smith");
            put("organizations", new HashMap(){{
                put("0", new HashMap(){{
                    put("id", "1234");
                }});
                put("abc", new HashMap(){{
                    put("id", "5678");
                }});
            }});
        }};

        return source;
    }
}

返されたMapは、 ReallyHeavyObject囲むインスタンスへの参照が含まれるようになりReallyHeavyObject 。 おそらくそれを危険にさらしたいとは思わない:

http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/の画像

3. Javaにマップリテラルがあるとふりかけることができます

あなたの実際の質問に答えるために、人々はこの構文を使って、Javaが既存の配列リテラルに似たマップリテラルのようなふりをしているように見せています。

String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};

構文上刺激的な人がいるかもしれません。





initialization