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 .
quelle
flavors
, ein zu seinHashSet
, aber leider ist es eine anonyme Unterklasse.Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Antworten:
Hier ist das Problem, wenn ich von anonymen inneren Klassen zu sehr mitgerissen werde:
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
class
Datei 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
class
Dateien.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:
Es sieht aus wie eine "versteckte" Funktion von Java, ist aber nur eine Umschreibung von:
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 :
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
ArrayList
s mit den Elementen"Hello"
und"World!"
fügen Sie sie über dieadd
Methode hinzu, indem Sie die beiden Methoden verwenden:Methode 1: Doppelklammerinitialisierung
Methode 2: Instanziieren Sie ein
ArrayList
undadd
Ich habe ein einfaches Programm zum Schreiben einer Java-Quelldatei erstellt, um 1000 Initialisierungen mit den beiden folgenden Methoden durchzuführen:
Test 1:
Test 2:
Bitte beachten Sie, dass die verstrichene Zeit zum Initialisieren der 1000
ArrayList
s und der 1000 anonymen inneren Klassen, die erweitertArrayList
werden, mit dem überprüft wirdSystem.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:
Wie zu sehen ist, hat die Doppelklammerinitialisierung eine merkliche Ausführungszeit von etwa 190 ms.
In der Zwischenzeit
ArrayList
betrug 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
.class
Dateien generiert, die durch das Kompilieren desTest1
Doppelklammer-Initialisierungstestprogramms generiert wurden .quelle
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).
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.Um klar zu sein, welche anonyme Unterklasse erstellt wird, können Sie dort auch Methoden definieren. Zum Beispiel überschreiben
HashSet.add()
quelle
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:
... wird diese Klassen produzieren:
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:
Die zurückgegebene
Map
enthält jetzt einen Verweis auf die einschließende Instanz vonReallyHeavyObject
. Sie wollen das wahrscheinlich nicht riskieren: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:
Einige Leute mögen dies syntaktisch anregend finden.
quelle
Nehmen Sie an der folgenden Testklasse teil:
und dann die Klassendatei dekompilieren, sehe ich:
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
this
bezieht 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:
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.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: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:
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
flavors
wird jedoch nicht dagegen,HashSet
sondern gegenMyHashSet
. 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 erstrecktHashSet
.Wenn Sie Fragen zu den Details der Instanzinitialisierer haben, lesen Sie die Details in der JLS- Dokumentation.
quelle
super()
als zweite Zeile des impliziten Konstruktors, aber es muss zuerst kommen. (Ich habe es getestet und es wird nicht kompiliert.)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.
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 sonew LinkedHashSet(Arrays.asList("xxx","YYY"));
quelle
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.
druckt
quelle
compareCollections
kombiniert 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.||
&&
&&
Zum Erstellen von Sets können Sie eine varargs-Factory-Methode anstelle der doppelten Klammerinitialisierung verwenden:
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.
quelle
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 ...)
:Die Einschränkung dieses Ansatzes besteht natürlich darin, dass Sie den zu generierenden Listentyp nicht steuern können.
quelle
new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))
, um dieses Problem zu umgehen .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.
quelle
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:
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 .
quelle
static
Kontexten verwendet wird). Der fehleranfällige Link am Ende meiner Frage erläutert dies weiter.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
Lass mich wissen was du denkst :)
quelle
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:
quelle
HashSet
vorgeschlagene Kapazität angeben - denken Sie an den Ladefaktor).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).
quelle
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:
und benutzt es so:
Google Collections, jetzt Teil von Guava, unterstützt eine ähnliche Idee für die Listenerstellung. In diesem Interview sagt Jared Levy:
7/10/2014: Wenn es nur so einfach sein könnte wie Pythons:
21.02.2020: In Java 11 können Sie jetzt sagen:
quelle
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.Es scheint mir, als ob der Kontext das Objekt ist, von dem zurückgegeben wird
new
, nämlich dasHashSet
.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.
quelle