908 Stimmen

Effizienz der Java "Double Brace Initialisierung"?

En Versteckte Funktionen von Java die erste Antwort erwähnt Initialisierung der doppelten Klammer , mit einer muy verlockende Syntax:

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

Dieses Idiom erzeugt eine anonyme innere Klasse, die nur einen Instanzinitialisierer enthält, der "alle [...] Methoden im enthaltenen Bereich verwenden kann".

Die wichtigste Frage: Ist das so ineffizient wie es klingt? Sollte ihre Verwendung auf einmalige Initialisierungen beschränkt werden? (Und natürlich auf Angeberei!)

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

Dritte Frage: Ist diese Redewendung zu undurchsichtig im Produktionscode zu verwenden?

Zusammenfassung: Sehr, sehr gute Antworten, vielen Dank an alle. Bei Frage (3) waren die Leute der Meinung, dass die Syntax klar sein sollte (obwohl ich einen gelegentlichen Kommentar empfehlen würde, vor allem, wenn Ihr Code an Entwickler weitergegeben wird, die damit vielleicht nicht vertraut sind).

Bei Frage (1) sollte der generierte Code schnell laufen. Die zusätzlichen .class-Dateien verursachen Unordnung in den jar-Dateien und verlangsamen den Programmstart etwas (Dank an @coobird für die Messung). @Thilo wies darauf hin, dass die Garbage Collection beeinträchtigt werden kann, und die Speicherkosten für die zusätzlich geladenen Klassen können in einigen Fällen ein Faktor sein.

Die Frage (2) war für mich am interessantesten. Wenn ich die Antworten verstehe, passiert in DBI, dass die anonyme innere Klasse die Klasse des Objekts erweitert, das durch den new-Operator konstruiert wird, und daher einen "this"-Wert hat, der auf die konstruierte Instanz verweist. Sehr ordentlich.

Insgesamt erscheint mir die DBI als eine Art intellektuelle Kuriosität. Coobird und andere weisen darauf hin, dass man denselben Effekt mit Arrays.asList, varargs-Methoden, Google Collections und den vorgeschlagenen Java 7 Collection-Literalen erzielen kann. Neuere JVM-Sprachen wie Scala, JRuby und Groovy bieten ebenfalls prägnante Notationen für die Listenkonstruktion und arbeiten gut mit Java zusammen. In Anbetracht der Tatsache, dass DBI den Klassenpfad unübersichtlich macht, das Laden von Klassen etwas verlangsamt und den Code ein wenig undurchsichtiger macht, würde ich es wahrscheinlich meiden. Allerdings habe ich vor, dies einem Freund zu empfehlen, der gerade seinen SCJP gemacht hat und sich gerne über die Java-Semantik streitet ;-) Vielen Dank an alle!

7/2017: Baeldung hat eine gute Zusammenfassung der doppelten Klammerinitialisierung und hält sie für ein Anti-Muster.

12/2017: @Basil Bourque stellt fest, dass man im neuen Java 9 sagen kann:

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

Das ist mit Sicherheit der richtige Weg. Wenn Sie mit einer früheren Version arbeiten müssen, schauen Sie sich folgende Seiten an Google Collections' ImmutableSet .

656voto

coobird Punkte 155882

Hier ist das Problem, wenn ich mich zu sehr von anonymen inneren Klassen hinreißen lasse:

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 alle Klassen, die generiert wurden, als ich eine einfache Anwendung erstellte und reichlich anonyme innere Klassen verwendete - jede Klasse wird in eine separate class Datei.

Die "Doppelklammerinitialisierung" ist, wie bereits erwähnt, eine anonyme innere Klasse mit einem Instanzinitialisierungsblock, was bedeutet, dass für jede "Initialisierung" eine neue Klasse erstellt wird, und das alles mit dem Ziel, normalerweise ein einziges Objekt zu erzeugen.

Wenn man bedenkt, dass die Java Virtual Machine all diese Klassen lesen muss, wenn man sie benutzt, kann das zu einer gewissen Zeit in der Bytecode-Prüfung Prozess und dergleichen. Ganz zu schweigen von der Vergrößerung des benötigten Festplattenspeichers, um all diese Daten zu speichern. class Dateien.

Es scheint, als ob es ein wenig Overhead bei der Verwendung von Double-Brace-Initialisierung, so ist es wahrscheinlich nicht so eine gute Idee, zu übertreiben mit ihm. Aber wie Eddie in den Kommentaren bemerkt hat, ist es nicht möglich, absolut sicher zu sein, die Auswirkungen.


Die Initialisierung der doppelten Klammer sieht wie folgt aus:

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

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

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

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

Es ist also im Grunde ein Instanzinitialisierungsblock die Teil eines anonyme innere Klasse .


Joshua Blochs Sammlung Literale Vorschlag para Projekt Münze war in etwa so:

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

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

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

Leider ist es hat sich nicht durchgesetzt weder in Java 7 noch 8 integriert und wurde auf unbestimmte Zeit verschoben.


Experiment

Hier ist das einfache Experiment, das ich getestet habe - machen Sie 1000 ArrayList s mit den Elementen "Hello" y "World!" die über die Funktion add Methode, wobei die beiden Methoden verwendet werden:

Methode 1: Doppelte Klammerinitialisierung

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

Methode 2: Instanziieren einer ArrayList y add

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

Ich habe ein einfaches Programm erstellt, um eine Java-Quelldatei zu schreiben, die 1000 Initialisierungen mit den beiden Methoden durchführt:

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 zur Initialisierung der 1000 ArrayList s und die 1000 anonymen inneren Klassen, die ArrayList wird mit der Funktion System.currentTimeMillis Der Timer hat also keine sehr hohe Auflösung. Auf meinem Windows-System beträgt die Auflösung etwa 15-16 Millisekunden.

Die Ergebnisse von 10 Durchläufen der beiden Tests waren wie folgt:

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 man sieht, hat die Initialisierung der Doppelklammer eine auffällige Ausführungszeit von etwa 190 ms.

Inzwischen ist die ArrayList Die Ausführungszeit für die Initialisierung betrug 0 ms. Natürlich sollte die Auflösung des Zeitgebers berücksichtigt werden, aber sie dürfte unter 15 ms liegen.

Es scheint also einen deutlichen Unterschied in der Ausführungszeit der beiden Methoden zu geben. Es scheint in der Tat einen gewissen Overhead bei den beiden Initialisierungsmethoden zu geben.

Und ja, es waren 1000 .class Dateien, die durch Kompilieren der Test1 Testprogramm zur Initialisierung der Doppelklammer.

115voto

Thilo Punkte 248982

Eine Eigenschaft dieses Ansatzes, auf die bisher noch nicht hingewiesen wurde, ist, dass durch die Erstellung innerer Klassen die gesamte enthaltende Klasse in ihrem Geltungsbereich erfasst wird. Das bedeutet, dass Ihr Set, solange es existiert, einen Zeiger auf die enthaltende Instanz behält ( this$0 ) und verhindern, dass dieser Müll gesammelt wird, was ein Problem darstellen könnte.

Dies und die Tatsache, dass eine neue Klasse überhaupt erst erstellt wird, obwohl ein normales HashSet genau richtig (oder sogar besser) funktionieren würde, lässt mich dieses Konstrukt nicht verwenden (obwohl ich mich wirklich nach dem syntaktischen Zucker sehne).

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

Genau so funktioniert der innere Unterricht. Sie bekommen ihre eigene this , aber sie haben auch Zeiger auf die übergeordnete Instanz, so dass Sie auch Methoden des enthaltenen Objekts aufrufen können. Im Falle eines Namenskonflikts hat die innere Klasse (in Ihrem Fall HashSet) Vorrang, aber Sie können dem "this" einen Klassennamen voranstellen, um auch die äußere Methode zu erhalten.

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 die Erstellung der anonymen Unterklasse zu verdeutlichen, könnten Sie dort auch Methoden definieren. Zum Beispiel überschreiben Sie HashSet.add()

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

            @Override
            boolean add(String s){

            }

        };
    }

73voto

Lukas Eder Punkte 194234

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

Abgesehen davon, dass die Syntax eher ungewöhnlich und nicht wirklich idiomatisch ist (über Geschmack lässt sich natürlich streiten), schaffen Sie damit unnötigerweise zwei erhebliche Probleme in Ihrer Anwendung, über die ich erst kürzlich hier in einem Blog ausführlicher berichtet habe .

1. Sie erstellen viel zu viele anonyme Klassen

Jedes Mal, wenn Sie die doppelte Klammerinitialisierung verwenden, wird eine neue Klasse erstellt. Z.B. 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 ein ziemlicher Overhead für Ihren Classloader - für nichts! Natürlich braucht es nicht viel Zeit für die Initialisierung, wenn Sie es einmal tun. Aber wenn Sie dies 20.000 Mal in Ihrer Unternehmensanwendung tun... all der Heap-Speicher nur für ein bisschen "Syntaxzucker"?

2. Sie schaffen möglicherweise ein Speicherleck!

Wenn Sie den obigen Code nehmen und diese Map von einer Methode zurückgeben, könnten die Aufrufer dieser Methode ahnungslos an sehr schweren Ressourcen festhalten, die nicht entsorgt 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ückgegebenen Map enthält nun einen Verweis auf die umschließende Instanz von ReallyHeavyObject . Das wollen Sie wahrscheinlich nicht riskieren:

Memory Leak Right Here

Bild aus <a href="http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/" rel="noreferrer">http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/</a>

3. Sie können so tun, als hätte Java Map-Literale

Um Ihre eigentliche Frage zu beantworten: Die Leute haben diese Syntax verwendet, um so zu tun, als ob Java so etwas wie Map-Literale hätte, ähnlich wie die bestehenden Array-Literale:

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

Manche Menschen mögen dies syntaktisch anregend finden.

41voto

Eddie Punkte 52504

Sie nehmen 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 sehr ineffizient aus. Wenn ich mir Sorgen um die Leistung bei so etwas machen würde, würde ich es profilieren. Und Ihre Frage #2 wird durch den obigen Code beantwortet: Sie befinden sich in einem impliziten Konstruktor (und Instanzinitialisierer) für Ihre innere Klasse, also " this " bezieht sich auf diese innere Klasse.

Ja, diese Syntax ist unklar, aber ein Kommentar kann die Verwendung einer unklaren Syntax klären. Zur Verdeutlichung der Syntax: Die meisten Menschen sind mit einem statischen Initialisierungsblock vertraut (JLS 8.7 Statische Initialisierungen):

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 verwenden (ohne das Wort " static ") für die Verwendung von Konstruktoren (JLS 8.6 Instanzinitialisierer), obwohl ich dies noch nie in 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, dann wird der Codeblock zwischen { y } wird vom Compiler in einen Konstruktor umgewandelt. Lösen Sie den Code der doppelten geschweiften Klammer vor diesem Hintergrund auf:

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

Der Codeblock zwischen den innersten geschweiften Klammern wird vom Compiler in einen Konstruktor umgewandelt. Die äußeren geschweiften Klammern grenzen die anonyme innere Klasse ab. Dies ist der letzte Schritt, 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 wenig, dass er vernachlässigt werden kann). Allerdings ist jede Verwendung von flavors wird sich nicht gegen HashSet sondern stattdessen gegen MyHashSet . Wahrscheinlich gibt es einen kleinen (und möglicherweise vernachlässigbaren) Mehraufwand dafür. Aber auch hier gilt: Bevor ich mir darüber Gedanken mache, würde ich ein Profil erstellen.

Nochmals zu Ihrer Frage Nr. 2: Der obige Code ist das logische und explizite Äquivalent zur Initialisierung der doppelten Klammer, und er macht deutlich, wo " this " verweist: Auf die innere Klasse, die die HashSet .

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

37voto

bestsss Punkte 11264

leckanfällig

Ich habe beschlossen, mich einzuschalten. Die Auswirkungen auf die Leistung umfassen: Festplattenoperation + Entpacken (für jar), Klassenüberprüfung, perm-gen Platz (für Suns Hotspot JVM). Das Schlimmste von allem ist jedoch, dass es zu Lecks kommt. Sie können nicht einfach zurückkehren.

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

Wenn also die Menge in einen anderen Teil ausweicht, der von einem anderen Classloader geladen wird, und dort ein Verweis aufbewahrt wird, wird der gesamte Baum von Klassen und Classloader durchsickern. Um dies zu vermeiden, ist eine Kopie in die HashMap erforderlich, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}) . Nicht mehr so niedlich. Ich selbst verwende die Redewendung nicht, stattdessen ist es wie new LinkedHashSet(Arrays.asList("xxx","YYY"));

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X