Effizienz der Java "Double Brace Initialization"?

823

In Hidden Features of Java wird in der Top-Antwort die Double Brace-Initialisierung mit einer sehr verlockenden Syntax erwähnt:

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

Diese Redewendung erstellt eine anonyme innere Klasse mit nur einem Instanzinitialisierer, die "beliebige [...] Methoden im enthaltenen Bereich verwenden kann".

Hauptfrage: Ist das so ineffizient, wie es sich anhört? Sollte seine Verwendung auf einmalige Initialisierungen beschränkt sein? (Und natürlich angeben!)

Zweite Frage: Das neue HashSet muss das "Dies" sein, das im Instanzinitialisierer verwendet wird ... kann jemand Licht in den Mechanismus bringen?

Dritte Frage: Ist diese Redewendung zu dunkel , um sie im Produktionscode zu verwenden?

Zusammenfassung: Sehr, sehr schöne Antworten, danke an alle. Bei Frage (3) waren die Leute der Meinung, dass die Syntax klar sein sollte (obwohl ich gelegentlich einen Kommentar empfehlen würde, insbesondere wenn Ihr Code an Entwickler weitergegeben wird, die möglicherweise nicht damit vertraut sind).

Bei Frage (1) sollte der generierte Code schnell ausgeführt werden. Die zusätzlichen .class-Dateien verursachen Unordnung in der JAR-Datei und verlangsamen den Programmstart geringfügig (danke an @coobird für die Messung). @Thilo wies darauf hin, dass die Speicherbereinigung betroffen sein kann und die Speicherkosten für die zusätzlich geladenen Klassen in einigen Fällen ein Faktor sein können.

Frage (2) erwies sich für mich als am interessantesten. Wenn ich die Antworten verstehe, passiert in DBI, dass die anonyme innere Klasse die Klasse des Objekts erweitert, das vom neuen Operator erstellt wird, und daher einen "this" -Wert hat, der auf die zu erstellende Instanz verweist. Sehr gepflegt.

Insgesamt erscheint mir DBI als eine Art intellektuelle Neugier. Coobird und andere weisen darauf hin, dass Sie mit Arrays.asList, varargs-Methoden, Google Collections und den vorgeschlagenen Java 7 Collection-Literalen denselben Effekt erzielen können. Neuere JVM-Sprachen wie Scala, JRuby und Groovy bieten auch präzise Notationen für die Listenerstellung und arbeiten gut mit Java zusammen. Angesichts der Tatsache, dass DBI den Klassenpfad überfüllt, das Laden der Klasse etwas verlangsamt und den Code etwas dunkler macht, würde ich mich wahrscheinlich davor scheuen. Ich habe jedoch vor, dies einem Freund zu überlassen, der gerade seinen SCJP erhalten hat und gutmütige Turniere über Java-Semantik liebt! ;-) Danke an alle!

7/2017: Baeldung hat eine gute Zusammenfassung der Doppelklammerinitialisierung und betrachtet sie als Anti-Muster.

12/2017: @Basil Bourque stellt fest, dass Sie im neuen Java 9 sagen können:

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

Das ist sicher der richtige Weg. Wenn Sie mit einer früheren Version nicht weiterkommen , schauen Sie sich das ImmutableSet von Google Collections an .

Jim Ferrans
quelle
33
Der Code-Geruch, den ich hier sehe, ist, dass der naive Leser erwarten würde flavors, ein zu sein HashSet, aber leider ist es eine anonyme Unterklasse.
Elazar Leibovich
6
Wenn Sie in Betracht ziehen, statt Leistung zu laden, gibt es keinen Unterschied, siehe meine Antwort.
Peter Lawrey
4
Ich finde es toll, dass Sie eine Zusammenfassung erstellt haben. Ich denke, dies ist eine lohnende Übung für Sie, um das Verständnis und die Community zu verbessern.
Patrick Murphy
3
Es ist meiner Meinung nach nicht dunkel. Leser sollten wissen, dass ein Doppel ... o warte, @ElazarLeibovich hat das bereits in seinem Kommentar gesagt . Der Double-Brace-Initialisierer selbst existiert nicht als Sprachkonstrukt, sondern ist nur eine Kombination aus einer anonymen Unterklasse und einem Instanzinitialisierer. Das einzige ist, dass die Leute sich dessen bewusst sein müssen.
MC Emperor
8
Java 9 bietet unveränderliche statische Factory-Methoden an , die in einigen Situationen die Verwendung von DCI ersetzen können:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque

Antworten:

607

Hier ist das Problem, wenn ich von anonymen inneren Klassen zu sehr mitgerissen werde:

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

Dies sind alles Klassen, die generiert wurden, als ich eine einfache Anwendung erstellte und reichlich anonyme innere Klassen verwendete - jede Klasse wird in einer separaten classDatei kompiliert .

Die "doppelte Klammerinitialisierung" ist, wie bereits erwähnt, eine anonyme innere Klasse mit einem Instanzinitialisierungsblock, was bedeutet, dass für jede "Initialisierung" eine neue Klasse erstellt wird, um normalerweise ein einzelnes Objekt zu erstellen.

Wenn man bedenkt, dass die Java Virtual Machine alle diese Klassen lesen muss, wenn sie verwendet werden, kann dies zu einer gewissen Zeit bei der Überprüfung des Bytecodes und dergleichen führen. Ganz zu schweigen von der Erhöhung des erforderlichen Speicherplatzes zum Speichern all dieser classDateien.

Es scheint, als ob bei der Verwendung der Double-Brace-Initialisierung ein wenig Overhead entsteht. Daher ist es wahrscheinlich keine so gute Idee, zu viel davon zu übertreiben. Aber wie Eddie in den Kommentaren bemerkt hat, ist es nicht möglich, absolut sicher zu sein, welche Auswirkungen dies hat.


Nur als Referenz ist die Initialisierung der doppelten Klammer die folgende:

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

Es sieht aus wie eine "versteckte" Funktion von Java, ist aber nur eine Umschreibung von:

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

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

Es handelt sich also im Grunde genommen um einen Instanzinitialisierungsblock , der Teil einer anonymen inneren Klasse ist .


Joshua Blochs Vorschlag für Collection Literals für Project Coin lautete wie folgt :

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

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

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

Leider es hat seinen Weg nicht in keinem der beiden Java 7 noch 8 und wurde auf unbestimmte Zeit.


Experiment

Hier ist das einfache Experiment, das ich getestet habe: Machen Sie 1000 ArrayLists mit den Elementen "Hello"und "World!"fügen Sie sie über die addMethode hinzu, indem Sie die beiden Methoden verwenden:

Methode 1: Doppelklammerinitialisierung

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

Methode 2: Instanziieren Sie ein ArrayListundadd

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

Ich habe ein einfaches Programm zum Schreiben einer Java-Quelldatei erstellt, um 1000 Initialisierungen mit den beiden folgenden Methoden durchzuführen:

Test 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);
  }
}

Test 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);
  }
}

Bitte beachten Sie, dass die verstrichene Zeit zum Initialisieren der 1000 ArrayLists und der 1000 anonymen inneren Klassen, die erweitert ArrayListwerden, mit dem überprüft wird System.currentTimeMillis, sodass der Timer keine sehr hohe Auflösung hat. Auf meinem Windows-System beträgt die Auflösung etwa 15-16 Millisekunden.

Die Ergebnisse für 10 Läufe der beiden Tests waren die folgenden:

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

Wie zu sehen ist, hat die Doppelklammerinitialisierung eine merkliche Ausführungszeit von etwa 190 ms.

In der Zwischenzeit ArrayListbetrug die Initialisierungsausführungszeit 0 ms. Natürlich sollte die Timer-Auflösung berücksichtigt werden, aber es ist wahrscheinlich, dass sie unter 15 ms liegt.

Es scheint also einen merklichen Unterschied in der Ausführungszeit der beiden Methoden zu geben. Es scheint, dass die beiden Initialisierungsmethoden tatsächlich einen gewissen Overhead aufweisen.

Und ja, es wurden 1000 .classDateien generiert, die durch das Kompilieren des Test1Doppelklammer-Initialisierungstestprogramms generiert wurden .

Coobird
quelle
10
"Wahrscheinlich" ist das maßgebliche Wort. Sofern nicht gemessen, sind keine Aussagen zur Leistung aussagekräftig.
Instance Hunter
16
Sie haben so gute Arbeit geleistet, dass ich das kaum sagen möchte, aber die Test1-Zeiten könnten von Klassenlasten dominiert werden. Es wäre interessant zu sehen, wie jemand eine einzelne Instanz jedes Tests in einer for-Schleife beispielsweise 1.000 Mal ausführt, sie dann in einer zweiten for-Schleife 1.000 oder 10.000 Mal erneut ausführt und den Zeitunterschied ausdruckt (System.nanoTime ()). Die erste for-Schleife sollte alle Aufwärmeffekte (JIT, Klassenlast, z. B.) überwinden. Beide Tests modellieren jedoch unterschiedliche Anwendungsfälle. Ich werde versuchen, dies morgen bei der Arbeit durchzuführen.
Jim Ferrans
8
@ Jim Ferrans: Ich bin mir ziemlich sicher, dass die Test1-Zeiten von Klassenlasten stammen. Die Konsequenz der Verwendung der Doppelklammerinitialisierung besteht jedoch darin, dass Klassenlasten bewältigt werden müssen. Ich glaube, die meisten Anwendungsfälle für Double Brace Init. Für die einmalige Initialisierung ist der Test unter Bedingungen näher an einem typischen Anwendungsfall dieser Art der Initialisierung. Ich würde glauben, dass mehrere Iterationen jedes Tests die Ausführungszeitlücke verkleinern würden.
Coobird
73
Dies beweist, dass a) die Doppelklammerinitialisierung langsamer ist und b) selbst wenn Sie dies 1000 Mal tun, Sie den Unterschied wahrscheinlich nicht bemerken werden. Und es ist auch nicht so, dass dies der Engpass in einer inneren Schleife sein könnte. Es verhängt eine winzige einmalige Strafe am schlimmsten.
Michael Myers
15
Wenn die Verwendung von DBI den Code lesbarer oder aussagekräftiger macht, verwenden Sie ihn. Die Tatsache, dass dadurch die Arbeit der JVM etwas erhöht wird, ist an sich kein gültiges Argument dagegen. Wenn ja, dann sollten wir uns auch Sorgen um zusätzliche Hilfsmethoden / -klassen machen und stattdessen große Klassen mit weniger Methoden bevorzugen ...
Rogério
105

Eine Eigenschaft dieses Ansatzes, auf die bisher nicht hingewiesen wurde, ist, dass beim Erstellen innerer Klassen die gesamte enthaltende Klasse in ihrem Umfang erfasst wird. Dies bedeutet, dass Ihr Set, solange es aktiv ist, einen Zeiger auf die enthaltende Instanz ( this$0) beibehält und verhindert, dass dieser durch Müll gesammelt wird, was ein Problem sein könnte.

Dies und die Tatsache, dass eine neue Klasse überhaupt erst erstellt wird, obwohl ein reguläres HashSet gut (oder sogar noch besser) funktionieren würde, macht mich nicht bereit, dieses Konstrukt zu verwenden (obwohl ich mich wirklich nach dem syntaktischen Zucker sehne).

Zweite Frage: Das neue HashSet muss das "Dies" sein, das im Instanzinitialisierer verwendet wird ... kann jemand Licht in den Mechanismus bringen? Ich hätte naiv erwartet, dass sich "dies" auf das Objekt bezieht, das "Aromen" initialisiert.

So funktionieren innere Klassen. Sie erhalten ihre eigenen this, haben aber auch Zeiger auf die übergeordnete Instanz, sodass Sie auch Methoden für das enthaltende Objekt aufrufen können. Im Falle eines Namenskonflikts hat die innere Klasse (in Ihrem Fall HashSet) Vorrang, aber Sie können "this" einen Klassennamen voranstellen, um auch die äußere Methode abzurufen.

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 
            }
        };
    }
}

Um klar zu sein, welche anonyme Unterklasse erstellt wird, können Sie dort auch Methoden definieren. Zum Beispiel überschreibenHashSet.add()

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

            @Override
            boolean add(String s){

            }

        };
    }
Thilo
quelle
5
Sehr guter Punkt zum versteckten Verweis auf die enthaltende Klasse. Im ursprünglichen Beispiel ruft der Instanzinitialisierer die add () -Methode des neuen HashSet <String> auf, nicht Test.this.add (). Das deutet darauf hin, dass etwas anderes passiert. Gibt es eine anonyme innere Klasse für das HashSet <String>, wie Nathan Kitchen vorschlägt?
Jim Ferrans
Der Verweis auf die enthaltende Klasse kann auch gefährlich sein, wenn die Serialisierung der Datenstruktur beteiligt ist. Die enthaltende Klasse wird ebenfalls serialisiert und muss daher serialisierbar sein. Dies kann zu undurchsichtigen Fehlern führen.
Nur ein weiterer Java-Programmierer
56

Jedes Mal, wenn jemand eine Doppelklammerinitialisierung verwendet, wird ein Kätzchen getötet.

Abgesehen davon, dass die Syntax eher ungewöhnlich und nicht wirklich idiomatisch ist (Geschmack ist natürlich umstritten), verursachen Sie unnötigerweise zwei signifikante Probleme in Ihrer Anwendung, über die ich kürzlich hier ausführlicher gebloggt habe .

1. Sie erstellen viel zu viele anonyme Klassen

Jedes Mal, wenn Sie die doppelte Klammerinitialisierung verwenden, wird eine neue Klasse erstellt. ZB dieses Beispiel:

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");
        }});
    }});
}};

... wird diese Klassen produzieren:

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

Das ist ziemlich viel Aufwand für Ihren Klassenlader - für nichts! Natürlich dauert die Initialisierung nicht lange, wenn Sie dies einmal tun. Aber wenn Sie dies 20'000 Mal in Ihrer Unternehmensanwendung tun ... all diesen Heap-Speicher nur für ein bisschen "Syntaxzucker"?

2. Sie verursachen möglicherweise einen Speicherverlust!

Wenn Sie den obigen Code verwenden und diese Zuordnung von einer Methode zurückgeben, halten Aufrufer dieser Methode möglicherweise ahnungslos an sehr schweren Ressourcen fest, die nicht durch Müll gesammelt werden können. Betrachten Sie das folgende Beispiel:

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;
    }
}

Die zurückgegebene Mapenthält jetzt einen Verweis auf die einschließende Instanz von ReallyHeavyObject. Sie wollen das wahrscheinlich nicht riskieren:

Speicherleck genau hier

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

3. Sie können so tun, als hätte Java Kartenliterale

Um Ihre eigentliche Frage zu beantworten, haben die Leute diese Syntax verwendet, um vorzutäuschen, dass Java so etwas wie Kartenliterale hat, ähnlich den vorhandenen Array-Literalen:

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

Einige Leute mögen dies syntaktisch anregend finden.

Lukas Eder
quelle
7
Rette die Kätzchen! Gute Antwort!
Aris2World
36

Nehmen Sie an der folgenden Testklasse teil:

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

und dann die Klassendatei dekompilieren, sehe ich:

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");
      }
    };
  }
}

Das sieht für mich nicht besonders ineffizient aus. Wenn ich mir wegen so etwas Sorgen um die Leistung machen würde, würde ich es profilieren. Und Ihre Frage Nr. 2 wird durch den obigen Code beantwortet: Sie befinden sich in einem impliziten Konstruktor (und Instanzinitialisierer) für Ihre innere Klasse, daher thisbezieht sich " " auf diese innere Klasse.

Ja, diese Syntax ist unklar, aber ein Kommentar kann die Verwendung der obskuren Syntax verdeutlichen. Um die Syntax zu verdeutlichen, sind die meisten Benutzer mit einem statischen Initialisierungsblock (JLS 8.7 Static Initializers) vertraut:

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

Sie können auch eine ähnliche Syntax (ohne das Wort " static") für die Verwendung von Konstruktoren verwenden (JLS 8.6 Instance Initializers), obwohl ich diese noch nie im Produktionscode gesehen habe. Dies ist viel weniger bekannt.

public class Sample2 {
    private final String someVar;

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

Wenn Sie keinen Standardkonstruktor haben, wird der Codeblock zwischen {und }vom Compiler in einen Konstruktor umgewandelt. In diesem Sinne entwirren Sie den Code der doppelten Klammer:

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

Der Codeblock zwischen den innersten Klammern wird vom Compiler in einen Konstruktor umgewandelt. Die äußersten Klammern begrenzen die anonyme innere Klasse. Um dies als letzten Schritt zu tun, um alles nicht anonym zu machen:

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

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

Für Initialisierungszwecke würde ich sagen, dass es überhaupt keinen Overhead gibt (oder so klein, dass er vernachlässigt werden kann). Jeder Gebrauch von flavorswird jedoch nicht dagegen, HashSetsondern gegen MyHashSet. Dies hat wahrscheinlich einen geringen (und möglicherweise vernachlässigbaren) Aufwand zur Folge. Aber noch einmal, bevor ich mir darüber Sorgen machte, würde ich es profilieren.

Wiederum ist der obige Code für Ihre Frage Nr. 2 das logische und explizite Äquivalent der Doppelklammerinitialisierung und macht deutlich, wo sich " this" bezieht: Auf die innere Klasse, die sich erstreckt HashSet.

Wenn Sie Fragen zu den Details der Instanzinitialisierer haben, lesen Sie die Details in der JLS- Dokumentation.

Eddie
quelle
Eddie, sehr nette Erklärung. Wenn die JVM-Bytecodes so sauber sind wie die Dekompilierung, ist die Ausführungsgeschwindigkeit schnell genug, obwohl ich mir über die zusätzliche Unordnung in der Klasse etwas Sorgen machen würde. Ich bin immer noch gespannt, warum der Konstruktor des Instanzinitialisierers "this" als neue HashSet <String> -Instanz und nicht als Testinstanz ansieht. Ist dies nur ein explizit angegebenes Verhalten in der neuesten Java-Sprachspezifikation, um die Redewendung zu unterstützen?
Jim Ferrans
Ich habe meine Antwort aktualisiert. Ich habe das Boilerplate der Testklasse weggelassen, was die Verwirrung verursacht hat. Ich habe es in meine Antwort aufgenommen, um die Dinge offensichtlicher zu machen. Ich erwähne auch den JLS-Abschnitt für die in dieser Redewendung verwendeten Instanzinitialisierungsblöcke.
Eddie
1
@ Jim Die Interpretation von "dies" ist kein Sonderfall; Es bezieht sich einfach auf die Instanz der innersten umschließenden Klasse, bei der es sich um die anonyme Unterklasse von HashSet <String> handelt.
Nathan Kitchen
Tut mir leid, dass ich viereinhalb Jahre später einspringen muss. Aber das Schöne an der dekompilierten Klassendatei (Ihrem zweiten Codeblock) ist, dass sie kein gültiges Java ist! Es hat super()als zweite Zeile des impliziten Konstruktors, aber es muss zuerst kommen. (Ich habe es getestet und es wird nicht kompiliert.)
Chiastic-Security
1
@ chiastic-security: Manchmal generieren Dekompiler Code, der nicht kompiliert werden kann.
Eddie
35

leckanfällig

Ich habe mich entschlossen, mitzumachen. Die Auswirkungen auf die Leistung umfassen: Festplattenbetrieb + Entpacken (für JAR), Klassenüberprüfung, Speicherplatz für Perms (für Suns Hotspot JVM). Das Schlimmste ist jedoch, dass es leckanfällig ist. Sie können nicht einfach zurückkehren.

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

Wenn die Menge also zu einem anderen Teil gelangt, der von einem anderen Klassenladeprogramm geladen wurde, und dort eine Referenz gespeichert wird, wird der gesamte Baum von Klassen + Klassenladeprogramm durchgesickert. Um dies zu vermeiden, ist eine Kopie nach HashMap erforderlich new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). Nicht mehr so ​​süß. Ich selbst benutze die Redewendung nicht, stattdessen ist es so new LinkedHashSet(Arrays.asList("xxx","YYY"));

Bests
quelle
3
Glücklicherweise ist PermGen ab Java 8 keine Sache mehr. Ich denke, es gibt immer noch Auswirkungen, aber keine mit einer möglicherweise sehr undurchsichtigen Fehlermeldung.
Joey
2
@Joey, macht keinen Unterschied, ob der Speicher direkt vom GC (perm gen) verwaltet wird oder nicht. Ein Leck in Metaspace ist immer noch ein Leck, es sei denn, Meta ist begrenzt, es wird kein OOM (außerhalb der Dauerwelle) von Dingen wie oom_killer in Linux geben.
Bestsss
19

Das Laden vieler Klassen kann dem Start einige Millisekunden hinzufügen. Wenn der Start nicht so kritisch ist und Sie die Effizienz der Klassen nach dem Start betrachten, gibt es keinen Unterschied.

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();
    }
}

druckt

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
Peter Lawrey
quelle
2
Kein Unterschied, außer dass Ihr PermGen-Raum verdunstet, wenn DBI übermäßig verwendet wird. Zumindest wird dies der Fall sein , es sei denn, Sie legen einige unklare JVM-Optionen fest , um das Entladen von Klassen und die Speicherbereinigung des PermGen-Speicherplatzes zu ermöglichen. Angesichts der Verbreitung von Java als serverseitige Sprache muss das Speicher- / PermGen-Problem zumindest erwähnt werden.
Aroth
1
@aroth das ist ein guter Punkt. Ich gebe zu, dass ich in 16 Jahren Arbeit an Java noch nie an einem System gearbeitet habe, bei dem Sie PermGen (oder Metaspace) optimieren mussten. Für die Systeme, an denen ich gearbeitet habe, wurde die Größe des Codes immer relativ klein gehalten.
Peter Lawrey
2
Sollten die Bedingungen nicht eher mit als compareCollectionskombiniert werden ? Die Verwendung scheint nicht nur semantisch falsch zu sein, sondern wirkt auch der Absicht entgegen, die Leistung zu messen, da überhaupt nur die erste Bedingung getestet wird. Außerdem kann ein intelligenter Optimierer erkennen, dass sich die Bedingungen während der Iterationen niemals ändern werden. ||&&&&
Holger
@aroth nur als Update: Seit Java 8 verwendet die VM kein Perm-Gen mehr.
Angel O'Sphere
16

Zum Erstellen von Sets können Sie eine varargs-Factory-Methode anstelle der doppelten Klammerinitialisierung verwenden:

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

Die Google Sammlungsbibliothek bietet viele praktische Methoden wie diese sowie viele andere nützliche Funktionen.

Was die Dunkelheit der Redewendung betrifft, stoße ich darauf und verwende sie die ganze Zeit im Produktionscode. Ich würde mir mehr Sorgen machen über Programmierer, die verwirrt sind, dass die Redewendung Produktionscode schreiben darf.

Nat
quelle
Hah! ;-) Ich bin eigentlich ein Rip van Winkle, der seit 1,2 Tagen zu Java zurückkehrt (ich habe den VoiceXML-Voice-Webbrowser unter evolution.voxeo.com in Java geschrieben). Es hat Spaß gemacht, Generika, parametrisierte Typen, Sammlungen, java.util.concurrent, die neue Syntax für Schleifen usw. zu lernen. Es ist jetzt eine bessere Sprache. Obwohl der Mechanismus hinter DBI zunächst unklar erscheint, sollte die Bedeutung des Codes für Sie klar sein.
Jim Ferrans
10

Abgesehen von der Effizienz wünsche ich mir selten eine deklarative Sammlung außerhalb von Komponententests. Ich glaube, dass die Syntax der doppelten Klammer sehr gut lesbar ist.

Eine andere Möglichkeit, die deklarative Erstellung von Listen spezifisch zu erreichen, besteht darin, Folgendes zu verwenden Arrays.asList(T ...):

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

Die Einschränkung dieses Ansatzes besteht natürlich darin, dass Sie den zu generierenden Listentyp nicht steuern können.

Paul Morie
quelle
1
Arrays.asList () würde ich normalerweise verwenden, aber Sie haben Recht, diese Situation tritt hauptsächlich bei Komponententests auf. Echter Code würde die Listen aus DB-Abfragen, XML usw. erstellen.
Jim Ferrans
7
Beachten Sie jedoch asList: Die zurückgegebene Liste unterstützt das Hinzufügen oder Entfernen von Elementen nicht. Immer wenn ich asList verwende, übergebe ich die resultierende Liste an einen Konstruktor new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate")), um dieses Problem zu umgehen .
Michael Myers
7

Es ist im Allgemeinen nichts besonders ineffizientes daran. Für die JVM ist es im Allgemeinen nicht wichtig, dass Sie eine Unterklasse erstellt und einen Konstruktor hinzugefügt haben - das ist eine normale, alltägliche Aufgabe in einer objektorientierten Sprache. Ich kann mir ziemlich erfundene Fälle vorstellen, in denen Sie dadurch eine Ineffizienz verursachen könnten (z. B. haben Sie eine wiederholt aufgerufene Methode, die aufgrund dieser Unterklasse eine Mischung verschiedener Klassen verwendet, während die übergebene Klasse normalerweise völlig vorhersehbar wäre). - Im letzteren Fall könnte der JIT-Compiler Optimierungen vornehmen, die im ersten Fall nicht möglich sind. Aber wirklich, ich denke, die Fälle, in denen es darauf ankommt, sind sehr erfunden.

Ich würde das Problem eher unter dem Gesichtspunkt sehen, ob Sie mit vielen anonymen Klassen "Unordnung schaffen" möchten. Verwenden Sie als grobe Richtlinie die Verwendung der Redewendung nicht mehr als beispielsweise anonyme Klassen für Ereignishandler.

In (2) befinden Sie sich im Konstruktor eines Objekts. "Dies" bezieht sich also auf das Objekt, das Sie erstellen. Das unterscheidet sich nicht von anderen Konstruktoren.

Was (3) betrifft, hängt das wirklich davon ab, wer Ihren Code verwaltet, denke ich. Wenn Sie dies nicht im Voraus wissen, ist ein Benchmark, den ich vorschlagen würde, "Sehen Sie dies im Quellcode des JDK?" (In diesem Fall kann ich mich nicht erinnern, viele anonyme Initialisierer gesehen zu haben, und schon gar nicht in Fällen, in denen dies der einzige Inhalt der anonymen Klasse ist.) In den meisten mittelgroßen Projekten würde ich behaupten, dass Sie Ihre Programmierer wirklich brauchen werden, um die JDK-Quelle irgendwann zu verstehen, daher ist jede dort verwendete Syntax oder Redewendung "faires Spiel". Darüber hinaus würde ich sagen, schulen Sie Leute in dieser Syntax, wenn Sie die Kontrolle darüber haben, wer den Code verwaltet, oder kommentieren oder vermeiden Sie.

Neil Coffey
quelle
5

Die Double-Brace-Initialisierung ist ein unnötiger Hack, der zu Speicherlecks und anderen Problemen führen kann

Es gibt keinen legitimen Grund, diesen "Trick" anzuwenden. Guava bietet schöne unveränderliche Sammlungen , die sowohl statische Fabriken als auch Builder enthalten. So können Sie Ihre Sammlung dort auffüllen, wo sie in einer sauberen, lesbaren und sicheren Syntax deklariert ist .

Das Beispiel in der Frage lautet:

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

Dies ist nicht nur kürzer und leichter zu lesen, sondern vermeidet auch die zahlreichen Probleme mit dem in anderen Antworten beschriebenen doppelstrebigen Muster . Sicher, es funktioniert ähnlich wie ein direkt konstruiertes HashMap, aber es ist gefährlich und fehleranfällig, und es gibt bessere Optionen.

Jedes Mal, wenn Sie eine doppelte Initialisierung in Betracht ziehen, sollten Sie Ihre APIs erneut überprüfen oder neue einführen, um das Problem richtig anzugehen, anstatt syntaktische Tricks zu nutzen.

Fehleranfällig kennzeichnet jetzt dieses Anti-Muster .

dimo414
quelle
-1. Trotz einiger gültiger Punkte lautet diese Antwort: "Wie vermeide ich das Erzeugen unnötiger anonymer Klassen? Verwenden Sie ein Framework mit noch mehr Klassen!"
Agent_L
1
Ich würde sagen, es läuft darauf hinaus, "das richtige Tool für den Job zu verwenden, anstatt einen Hack, der Ihre Anwendung zum Absturz bringen kann". Guava ist eine ziemlich häufige Bibliothek für Anwendungen (Sie verpassen sie definitiv, wenn Sie sie nicht verwenden), aber selbst wenn Sie sie nicht verwenden möchten, können und sollten Sie die Initialisierung in doppelten Klammern vermeiden.
dimo414
Und wie genau würde eine doppelte Klammerinitialisierung ein meory Leck verursachen?
Angel O'Sphere
@ AngelO'Sphere DBI ist eine verschleierte Methode zum Erstellen einer inneren Klasse und behält daher einen impliziten Verweis auf die einschließende Klasse bei (sofern dies nicht immer nur in staticKontexten verwendet wird). Der fehleranfällige Link am Ende meiner Frage erläutert dies weiter.
dimo414
Ich würde sagen, es ist Geschmackssache. Und es ist nichts wirklich Verschleiertes daran.
Angel O'Sphere
4

Ich habe dies untersucht und mich entschlossen, einen eingehenderen Test durchzuführen als den, der durch die gültige Antwort bereitgestellt wird.

Hier ist der Code: https://gist.github.com/4368924

und das ist meine Schlussfolgerung

Ich war überrascht, dass in den meisten Lauftests die interne Initiierung tatsächlich schneller war (in einigen Fällen fast doppelt so hoch). Bei der Arbeit mit großen Zahlen scheint der Nutzen nachzulassen.

Interessanterweise verliert der Fall, bei dem 3 Objekte in der Schleife erstellt werden, seinen Nutzen früher als in den anderen Fällen. Ich bin mir nicht sicher, warum dies geschieht, und es sollten weitere Tests durchgeführt werden, um zu Schlussfolgerungen zu gelangen. Das Erstellen konkreter Implementierungen kann dazu beitragen, dass die Klassendefinition nicht neu geladen werden muss (falls dies der Fall ist).

Es ist jedoch klar, dass in den meisten Fällen nicht viel Overhead für das Einzelobjektgebäude beobachtet wurde, selbst bei großen Zahlen.

Ein Nachteil wäre die Tatsache, dass jede der Initialen mit doppelter Klammer eine neue Klassendatei erstellt, die der Größe unserer Anwendung einen ganzen Festplattenblock hinzufügt (oder etwa 1 KB, wenn sie komprimiert wird). Ein kleiner Platzbedarf, aber wenn er an vielen Orten verwendet wird, kann er möglicherweise Auswirkungen haben. Verwenden Sie diese Option 1000 Mal, und Sie fügen Ihrer Anwendung möglicherweise eine ganze MiB hinzu, was möglicherweise in einer eingebetteten Umgebung von Bedeutung ist.

Meine Schlussfolgerung? Es kann in Ordnung sein, es zu verwenden, solange es nicht missbraucht wird.

Lass mich wissen was du denkst :)

Pablisco
quelle
2
Das ist kein gültiger Test. Der Code erstellt Objekte, ohne sie zu verwenden, sodass der Optimierer die gesamte Instanzerstellung ausschließen kann. Der einzige verbleibende Nebeneffekt ist das Fortschreiten der Zufallszahlenfolge, deren Overhead bei diesen Tests ohnehin alles andere überwiegt.
Holger
3

Ich stimme der Antwort von Nat zu, außer dass ich eine Schleife verwenden würde, anstatt die implizite Liste aus asList (elements) zu erstellen und sofort zu werfen:

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }
Lawrence Dol
quelle
1
Warum? Das neue Objekt wird im Eden-Bereich erstellt und erfordert daher nur zwei oder drei Zeigerzusätze zum Instanziieren. Die JVM stellt möglicherweise fest, dass sie niemals über den Methodenbereich hinausgeht, und weist sie daher dem Stapel zu.
Nat
Ja, es wird wahrscheinlich effizienter als dieser Code (obwohl Sie ihn verbessern können, indem Sie die HashSetvorgeschlagene Kapazität angeben - denken Sie an den Ladefaktor).
Tom Hawtin - Tackline
Nun, der HashSet-Konstruktor muss die Iteration sowieso durchführen, damit es nicht weniger effizient wird. Für die Wiederverwendung erstellter Bibliothekscode sollte immer bestrebt sein, den bestmöglichen Wert zu erzielen.
Lawrence Dol
3

Diese Syntax kann zwar praktisch sein, fügt jedoch auch viele dieser $ 0-Referenzen hinzu, da diese verschachtelt werden, und es kann schwierig sein, das Debuggen in die Initialisierer zu starten, es sei denn, für jeden werden Haltepunkte festgelegt. Aus diesem Grund empfehle ich, dies nur für banale Setter zu verwenden, insbesondere für Konstanten und Orte, an denen anonyme Unterklassen keine Rolle spielen (wie keine Serialisierung).

Eric Woodruff
quelle
3

Mario Gleichman beschreibt, wie man generische Java 1.5-Funktionen verwendet, um Scala List-Literale zu simulieren, obwohl Sie leider unveränderliche Listen haben.

Er definiert diese Klasse:

package literal;

public class collection {
    public static <T> List<T> List(T...elems){
        return Arrays.asList( elems );
    }
}

und benutzt es so:

import static literal.collection.List;
import static system.io.*;

public class CollectionDemo {
    public void demoList(){
        List<String> slist = List( "a", "b", "c" );
        List<Integer> iList = List( 1, 2, 3 );
        for( String elem : List( "a", "java", "list" ) )
            System.out.println( elem );
    }
}

Google Collections, jetzt Teil von Guava, unterstützt eine ähnliche Idee für die Listenerstellung. In diesem Interview sagt Jared Levy:

[...] Die am häufigsten verwendeten Funktionen, die in fast jeder von mir geschriebenen Java-Klasse vorkommen, sind statische Methoden, die die Anzahl sich wiederholender Tastenanschläge in Ihrem Java-Code verringern. Es ist so bequem, Befehle wie die folgenden eingeben zu können:

Map<OneClassWithALongName, AnotherClassWithALongName> = Maps.newHashMap();

List<String> animals = Lists.immutableList("cat", "dog", "horse");

7/10/2014: Wenn es nur so einfach sein könnte wie Pythons:

animals = ['cat', 'dog', 'horse']

21.02.2020: In Java 11 können Sie jetzt sagen:

animals = List.of(“cat”, “dog”, “horse”)

Jim Ferrans
quelle
2
  1. Dies wird add()für jedes Mitglied erforderlich sein. Wenn Sie einen effizienteren Weg finden, um Elemente in ein Hash-Set einzufügen, verwenden Sie diesen. Beachten Sie, dass die innere Klasse wahrscheinlich Müll generiert, wenn Sie diesbezüglich sensibel sind.

  2. Es scheint mir, als ob der Kontext das Objekt ist, von dem zurückgegeben wird new, nämlich das HashSet.

  3. Wenn Sie fragen müssen ... Wahrscheinlicher: Werden die Leute, die nach Ihnen kommen, das wissen oder nicht? Ist es leicht zu verstehen und zu erklären? Wenn Sie beide mit "Ja" beantworten können, können Sie sie gerne verwenden.

MC Kaiser
quelle