Efficacité de Java «Initialisation à double accolade»?

824

Dans les fonctions cachées de Java, la première réponse mentionne l' initialisation à double accolade , avec une syntaxe très séduisante:

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

Cet idiome crée une classe interne anonyme avec juste un initialiseur d'instance, qui "peut utiliser n'importe quelle méthode [...] dans la portée contenante".

Question principale: est-ce aussi inefficace que cela puisse paraître? Son utilisation doit-elle être limitée à des initialisations ponctuelles? (Et bien sûr, exhibant!)

Deuxième question: le nouveau HashSet doit être le "ceci" utilisé dans l'initialiseur d'instance ... quelqu'un peut-il faire la lumière sur le mécanisme?

Troisième question: cet idiome est-il trop obscur pour être utilisé dans le code de production?

Résumé: Très, très belles réponses, merci à tous. À la question (3), les gens ont estimé que la syntaxe devrait être claire (bien que je recommanderais un commentaire occasionnel, surtout si votre code est transmis aux développeurs qui ne le connaissent peut-être pas).

Sur la question (1), le code généré doit s'exécuter rapidement. Les fichiers .class supplémentaires provoquent un encombrement des fichiers jar et ralentissent légèrement le démarrage du programme (merci à @coobird d'avoir mesuré cela). @Thilo a souligné que la récupération de place peut être affectée, et le coût de la mémoire pour les classes supplémentaires chargées peut être un facteur dans certains cas.

La question (2) s'est avérée être la plus intéressante pour moi. Si je comprends les réponses, ce qui se passe dans DBI est que la classe interne anonyme étend la classe de l'objet en cours de construction par le nouvel opérateur, et a donc une valeur "this" faisant référence à l'instance en cours de construction. Très propre.

Dans l'ensemble, DBI me semble être une sorte de curiosité intellectuelle. Coobird et d'autres soulignent que vous pouvez obtenir le même effet avec Arrays.asList, les méthodes varargs, Google Collections et les littéraux Java 7 Collection proposés. Les langages JVM plus récents comme Scala, JRuby et Groovy offrent également des notations concises pour la construction de listes et interagissent bien avec Java. Étant donné que DBI encombre le chemin de classe, ralentit un peu le chargement des classes et rend le code un peu plus obscur, je préfèrerais probablement l'éviter. Cependant, je prévois de lancer ceci sur un ami qui vient de recevoir son SCJP et aime les joutes de bonne nature sur la sémantique Java! ;-) Merci tout le monde!

7/2017: Baeldung a un bon résumé de l'initialisation à double accolade et le considère comme un anti-modèle.

12/2017: @Basil Bourque note que dans le nouveau Java 9 vous pouvez dire:

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

C'est à coup sûr la voie à suivre. Si vous êtes bloqué avec une version antérieure, jetez un œil à ImmutableSet de Google Collections .

Jim Ferrans
la source
33
L'odeur de code que je vois ici est que le lecteur naïf s'attendrait flavorsà être un HashSet, mais hélas c'est une sous-classe anonyme.
Elazar Leibovich, le
6
Si vous envisagez de courir au lieu de charger les performances, il n'y a pas de différence, consultez ma réponse.
Peter Lawrey
4
J'adore le fait que vous ayez créé un résumé, je pense que c'est un exercice utile pour vous et pour augmenter la compréhension et la communauté.
Patrick Murphy
3
Ce n'est pas obscur à mon avis. Les lecteurs doivent savoir qu'un double ... o attendez, @ElazarLeibovich l'a déjà dit dans son commentaire . L'initialiseur à double accolade lui-même n'existe pas en tant que construction de langage, il s'agit simplement d'une combinaison d'une sous-classe anonyme et d'un initialiseur d'instance. La seule chose est que les gens doivent être conscients de cela.
MC Emperor
8
Java 9 propose des méthodes d'usine statiques définies immuables qui peuvent remplacer l'utilisation de DCI dans certaines situations:Set<String> flavors = Set.of( "vanilla" , "strawberry" , "chocolate" , "butter pecan" ) ;
Basil Bourque

Réponses:

607

Voici le problème lorsque je m'emballe trop avec les classes internes anonymes:

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

Ce sont toutes des classes qui ont été générées lorsque je faisais une application simple, et utilisaient de grandes quantités de classes internes anonymes - chaque classe sera compilée dans un classfichier séparé .

La "double initialisation d'accolade", comme déjà mentionné, est une classe interne anonyme avec un bloc d'initialisation d'instance, ce qui signifie qu'une nouvelle classe est créée pour chaque "initialisation", le tout dans le but de créer généralement un seul objet.

Étant donné que la machine virtuelle Java devra lire toutes ces classes lors de leur utilisation, cela peut entraîner un certain temps dans le processus de vérification du bytecode et autres. Sans parler de l'augmentation de l'espace disque nécessaire pour stocker tous ces classfichiers.

Il semble qu'il y ait un peu de surcharge lors de l'utilisation de l'initialisation à double accolade, donc ce n'est probablement pas une si bonne idée d'aller trop loin avec. Mais comme Eddie l'a noté dans les commentaires, il n'est pas possible d'être absolument sûr de l'impact.


Juste pour référence, l'initialisation à double accolade est la suivante:

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

Cela ressemble à une fonctionnalité "cachée" de Java, mais ce n'est qu'une réécriture de:

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

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

Il s'agit donc essentiellement d'un bloc d'initialisation d'instance qui fait partie d'une classe interne anonyme .


La proposition de Joshua Bloch Collection Literals pour Project Coin allait dans le sens de:

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

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

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

Malheureusement, il n'a fait son chemin ni dans Java 7 ni dans 8 et a été mis en suspens indéfiniment.


Expérience

Voici l'expérience simple que j'ai testée - faites 1000 ArrayLists avec les éléments "Hello"et "World!"ajoutez-les via la addméthode, en utilisant les deux méthodes:

Méthode 1: initialisation à double accolade

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

Méthode 2: instancier un ArrayListetadd

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

J'ai créé un programme simple pour écrire un fichier source Java pour effectuer 1000 initialisations en utilisant les deux méthodes:

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

Veuillez noter que le temps écoulé pour initialiser les 1000 ArrayLists et les 1000 classes internes anonymes s'étendant ArrayListest vérifié à l'aide de la System.currentTimeMillis, donc la minuterie n'a pas une très haute résolution. Sur mon système Windows, la résolution est d'environ 15 à 16 millisecondes.

Les résultats pour 10 essais des deux tests étaient les suivants:

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

Comme on peut le voir, l'initialisation à double accolade a un temps d'exécution notable d'environ 190 ms.

Pendant ce temps, le ArrayListtemps d'exécution de l' initialisation s'est avéré être de 0 ms. Bien sûr, la résolution du temporisateur doit être prise en compte, mais elle est probablement inférieure à 15 ms.

Ainsi, il semble y avoir une différence notable dans le temps d'exécution des deux méthodes. Il semble bien qu'il y ait effectivement des frais généraux dans les deux méthodes d'initialisation.

Et oui, il y avait 1000 .classfichiers générés par la compilation du Test1programme de test d'initialisation à double accolade.

coobird
la source
10
"Probablement" étant le mot clé. À moins d'être mesurés, aucun énoncé sur la performance n'est significatif.
Instance Hunter
16
Vous avez fait un si bon travail que je veux à peine dire cela, mais les temps de Test1 pourraient être dominés par des charges de classe. Il serait intéressant de voir quelqu'un exécuter une seule instance de chaque test dans une boucle for, disons 1000 fois, puis l'exécuter à nouveau dans une seconde pour une boucle de 1000 ou 10000 fois et imprimer le décalage horaire (System.nanoTime ()). La première boucle for devrait dépasser tous les effets d'échauffement (JIT, chargement de classe, par exemple). Les deux tests modélisent cependant des cas d'utilisation différents. J'essaierai de faire ça demain au travail.
Jim Ferrans
8
@Jim Ferrans: Je suis assez certain que les heures de Test1 proviennent de charges de classe. Mais, la conséquence de l'utilisation de l'initialisation à double accolade est de devoir faire face aux charges de classe. Je crois que la plupart des cas d'utilisation pour l'init double accolade. est pour une initialisation unique, le test est plus proche dans des conditions d'un cas d'utilisation typique de ce type d'initialisation. Je pense que plusieurs itérations de chaque test réduiraient l'écart de temps d'exécution.
coobird
73
Cela prouve que a) l'initialisation à double accolade est plus lente et b) même si vous le faites 1000 fois, vous ne remarquerez probablement pas la différence. Et ce n'est pas comme si cela pouvait être le goulot d'étranglement dans une boucle intérieure. Il impose une minuscule pénalité unique AU TRÈS PIRE.
Michael Myers
15
Si l'utilisation de DBI rend le code plus lisible ou expressif, utilisez-le. Le fait qu'il augmente un peu le travail que la JVM doit effectuer n'est pas en soi un argument valable contre lui. Si c'était le cas, alors nous devrions également nous inquiéter des méthodes / classes auxiliaires supplémentaires, préférant plutôt des classes énormes avec moins de méthodes ...
Rogério
105

Une propriété de cette approche qui n'a pas été soulignée jusqu'à présent est que parce que vous créez des classes internes, toute la classe contenant est capturée dans sa portée. Cela signifie que tant que votre Set est vivant, il conservera un pointeur sur l'instance contenante ( this$0) et l'empêchera d'être récupéré, ce qui pourrait être un problème.

Cela, et le fait qu'une nouvelle classe soit créée en premier lieu même si un HashSet ordinaire fonctionnerait très bien (ou même mieux), me donne envie de ne pas utiliser cette construction (même si j'ai vraiment envie du sucre syntaxique).

Deuxième question: le nouveau HashSet doit être le "ceci" utilisé dans l'initialiseur d'instance ... quelqu'un peut-il faire la lumière sur le mécanisme? Je me serais attendu à ce que "ceci" se réfère à l'objet initialisant "saveurs".

C'est ainsi que fonctionnent les classes internes. Ils obtiennent le leur this, mais ils ont également des pointeurs vers l'instance parent, de sorte que vous pouvez également appeler des méthodes sur l'objet conteneur. En cas de conflit de dénomination, la classe interne (dans votre cas, HashSet) a priorité, mais vous pouvez préfixer "this" avec un nom de classe pour obtenir également la méthode externe.

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

Pour être clair sur la sous-classe anonyme en cours de création, vous pouvez également y définir des méthodes. Par exemple, remplacerHashSet.add()

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

            @Override
            boolean add(String s){

            }

        };
    }
Thilo
la source
5
Très bon point sur la référence cachée à la classe contenante. Dans l'exemple d'origine, l'initialiseur d'instance appelle la méthode add () du nouveau HashSet <String>, pas Test.this.add (). Cela me suggère que quelque chose d'autre se passe. Existe-t-il une classe interne anonyme pour le HashSet <String>, comme le suggère Nathan Kitchen?
Jim Ferrans
La référence à la classe conteneur peut également être dangereuse si la sérialisation de la structure de données est impliquée. La classe contenant sera également sérialisée et doit donc être sérialisable. Cela peut conduire à des erreurs obscures.
Juste un autre programmeur Java le
56

Chaque fois que quelqu'un utilise l'initialisation à double accolade, un chaton est tué.

Outre que la syntaxe est plutôt inhabituelle et pas vraiment idiomatique (le goût est discutable, bien sûr), vous créez inutilement deux problèmes importants dans votre application, dont je viens récemment de bloguer plus en détail ici .

1. Vous créez trop de classes anonymes

Chaque fois que vous utilisez l'initialisation à double accolade, une nouvelle classe est créée. Par exemple, cet exemple:

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

... produira ces classes:

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

C'est un peu de surcharge pour votre chargeur de classe - pour rien! Bien sûr, cela ne prendra pas beaucoup de temps d'initialisation si vous le faites une fois. Mais si vous faites cela 20'000 fois dans votre application d'entreprise ... toute cette mémoire pour un peu de "sucre de syntaxe"?

2. Vous créez potentiellement une fuite de mémoire!

Si vous prenez le code ci-dessus et renvoyez cette carte à partir d'une méthode, les appelants de cette méthode peuvent s'accrocher sans trop de ressources à des ressources très lourdes qui ne peuvent pas être récupérées. Prenons l'exemple suivant:

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

Le retour Mapcontiendra désormais une référence à l'instance englobante de ReallyHeavyObject. Vous ne voulez probablement pas risquer que:

Fuite de mémoire ici

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

3. Vous pouvez prétendre que Java a des littéraux de carte

Pour répondre à votre question réelle, les gens ont utilisé cette syntaxe pour prétendre que Java a quelque chose comme des littéraux de carte, similaires aux littéraux de tableau existants:

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

Certaines personnes peuvent trouver cela stimulant sur le plan syntaxique.

Lukas Eder
la source
7
Sauvez les chatons! Bonne réponse!
Aris2World
36

Prendre la classe de test suivante:

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

puis décompiler le fichier de classe, je vois:

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

Cela ne me semble pas terriblement inefficace. Si j'étais inquiet à propos des performances pour quelque chose comme ça, je le profilerais. Le code ci-dessus répond à votre question n ° 2: vous êtes dans un constructeur implicite (et un initialiseur d'instance) pour votre classe interne, " this" fait donc référence à cette classe interne.

Oui, cette syntaxe est obscure, mais un commentaire peut clarifier l'utilisation d'une syntaxe obscure. Pour clarifier la syntaxe, la plupart des gens connaissent un bloc d'initialisation statique (JLS 8.7 Static Initializers):

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

Vous pouvez également utiliser une syntaxe similaire (sans le mot " static") pour l'utilisation du constructeur (initialiseurs d'instance JLS 8.6), bien que je n'ai jamais vu cela utilisé dans le code de production. C'est beaucoup moins connu.

public class Sample2 {
    private final String someVar;

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

Si vous n'avez pas de constructeur par défaut, le bloc de code entre {et }est transformé en constructeur par le compilateur. Dans cet esprit, démêlez le code à double accolade:

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

Le bloc de code entre les accolades les plus internes est transformé en constructeur par le compilateur. Les accolades les plus externes délimitent la classe interne anonyme. Pour faire cela, la dernière étape de tout rendre non anonyme:

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

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

À des fins d'initialisation, je dirais qu'il n'y a pas de surcharge (ou si petite qu'elle puisse être négligée). Cependant, chaque utilisation de flavorsn'ira pas contre, HashSetmais contre MyHashSet. Il y a probablement un petit (et probablement négligeable) frais généraux à cela. Mais encore une fois, avant de m'en inquiéter, je le profilais.

Encore une fois, à votre question n ° 2, le code ci-dessus est l'équivalent logique et explicite de l'initialisation à double accolade, et il rend évident où " this" se réfère: à la classe interne qui s'étend HashSet.

Si vous avez des questions sur les détails des initialiseurs d'instance, consultez les détails dans la documentation JLS .

Eddie
la source
Eddie, très belle explication. Si les codes d'octets JVM sont aussi propres que la décompilation, la vitesse d'exécution sera assez rapide, même si je serais quelque peu préoccupé par l'encombrement supplémentaire des fichiers .class. Je suis toujours curieux de savoir pourquoi le constructeur de l'initialiseur d'instance voit "ceci" comme la nouvelle instance de HashSet <String> et non l'instance de test. Est-ce simplement un comportement explicitement spécifié dans la dernière spécification de langage Java pour prendre en charge l'idiome?
Jim Ferrans
J'ai mis à jour ma réponse. J'ai laissé de côté le passe-partout de la classe Test, ce qui a provoqué la confusion. Je l'ai mis dans ma réponse pour rendre les choses plus évidentes. Je mentionne également la section JLS pour les blocs d'initialisation d'instance utilisés dans cet idiome.
Eddie
1
@Jim L'interprétation de "ceci" n'est pas un cas particulier; il fait simplement référence à l'instance de la classe englobante la plus interne, qui est la sous-classe anonyme de HashSet <String>.
Nathan Kitchen
Désolé de sauter dans quatre ans et demi plus tard. Mais la bonne chose à propos du fichier de classe décompilé (votre deuxième bloc de code) est qu'il n'est pas Java valide! Il a super()comme deuxième ligne du constructeur implicite, mais il doit venir en premier. (Je l'ai testé et il ne se compilera pas.)
chiastic-security
1
@ chiastic-security: Parfois, les décompilateurs génèrent du code qui ne se compile pas.
Eddie
35

sujettes aux fuites

J'ai décidé d'intervenir. L'impact sur les performances comprend: le fonctionnement du disque + décompresser (pour le bocal), la vérification de classe, l'espace perm-gen (pour la JVM Hotspot de Sun). Mais le pire de tout: il est sujet aux fuites. Vous ne pouvez pas simplement revenir.

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

Donc, si l'ensemble s'échappe à toute autre partie chargée par un chargeur de classe différent et qu'une référence y est conservée, l'arborescence entière des classes + chargeur de classe sera divulguée. Pour éviter cela, une copie HashMap est nécessaire, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). Plus si mignon. Je n'utilise pas l'idiome, moi-même, c'est plutôt new LinkedHashSet(Arrays.asList("xxx","YYY"));

bestsss
la source
3
Heureusement, depuis Java 8, PermGen n'est plus une chose. Il y a toujours un impact, je suppose, mais pas avec un message d'erreur potentiellement très obscur.
Joey
2
@Joey, fait zéro différence si la mémoire est gérée directement par le GC (perm gen) ou non. Une fuite dans metaspace est toujours une fuite, à moins que la méta ne soit limitée, il n'y aura pas de MOO (hors perm gen) par des trucs comme oom_killer sous linux va entrer en jeu.
bestsss
19

Le chargement de nombreuses classes peut ajouter quelques millisecondes au début. Si le démarrage n'est pas si critique et que vous regardez l'efficacité des classes après le démarrage, il n'y a pas de différence.

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

impressions

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
la source
2
Aucune différence, sauf que votre espace PermGen s'évapore si DBI est utilisé de manière excessive. Au moins, à moins que vous ne définissiez des options JVM obscures pour permettre le déchargement de classe et le garbage-collection de l'espace PermGen. Étant donné la prévalence de Java en tant que langage côté serveur, le problème de mémoire / PermGen mérite au moins une mention.
2015
1
@aroth c'est un bon point. J'avoue qu'en 16 ans de travail sur Java, je n'ai jamais travaillé sur un système où il fallait régler le PermGen (ou Metaspace). Pour les systèmes sur lesquels j'ai travaillé, la taille du code a toujours été raisonnablement petite.
Peter Lawrey
2
Les conditions ne devraient-elles pas compareCollectionsêtre combinées avec ||plutôt qu'avec &&? L'utilisation &&ne semble pas seulement sémantiquement incorrecte, elle contrecarre l'intention de mesurer les performances, car seule la première condition sera testée. De plus, un optimiseur intelligent peut reconnaître que les conditions ne changeront jamais pendant les itérations.
Holger
@aroth comme une mise à jour: depuis Java 8, la VM n'utilise plus de perm-gen.
Angel O'Sphere
16

Pour créer des ensembles, vous pouvez utiliser une méthode d'usine varargs au lieu de l'initialisation à double accolade:

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

La bibliothèque Google Collections propose de nombreuses méthodes pratiques comme celle-ci, ainsi que de nombreuses autres fonctionnalités utiles.

Quant à l'obscurité de l'idiome, je la rencontre et l'utilise tout le temps dans le code de production. Je serais plus préoccupé par les programmeurs qui sont confus par l'idiome autorisé à écrire du code de production.

Nat
la source
Hah! ;-) Je suis en fait un Rip van Winkle de retour à Java depuis 1,2 jours (j'ai écrit le navigateur Web vocal VoiceXML sur evolution.voxeo.com en Java). Cela a été amusant d'apprendre les génériques, les types paramétrés, les collections, java.util.concurrent, la nouvelle syntaxe de boucle, etc. C'est un meilleur langage maintenant. Selon vous, même si le mécanisme derrière DBI peut sembler obscur au premier abord, la signification du code devrait être assez claire.
Jim Ferrans
10

Mis à part l'efficacité, je me retrouve rarement à souhaiter la création d'une collection déclarative en dehors des tests unitaires. Je crois que la syntaxe à double accolade est très lisible.

Une autre façon de réaliser spécifiquement la construction déclarative des listes est d'utiliser Arrays.asList(T ...)comme ceci:

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

La limitation de cette approche est bien sûr que vous ne pouvez pas contrôler le type spécifique de liste à générer.

Paul Morie
la source
1
Arrays.asList () est ce que j'utiliserais normalement, mais vous avez raison, cette situation survient principalement dans les tests unitaires; le vrai code construirait les listes à partir des requêtes DB, XML, etc.
Jim Ferrans
7
Attention cependant à asList: la liste retournée ne prend pas en charge l'ajout ou la suppression d'éléments. Chaque fois que j'utilise asList, je passe la liste résultante dans un constructeur comme new ArrayList<String>(Arrays.asList("vanilla", "strawberry", "chocolate"))pour contourner ce problème.
Michael Myers
7

Il n'y a généralement rien de particulièrement inefficace à ce sujet. Peu importe pour la JVM que vous ayez créé une sous-classe et y ayez ajouté un constructeur - c'est une chose normale et quotidienne à faire dans un langage orienté objet. Je peux penser à des cas assez artificiels où vous pourriez provoquer une inefficacité en faisant cela (par exemple, vous avez une méthode appelée à plusieurs reprises qui finit par prendre un mélange de différentes classes en raison de cette sous-classe, alors que la classe ordinaire transmise serait totalement prévisible- - dans ce dernier cas, le compilateur JIT pourrait faire des optimisations qui ne sont pas réalisables dans le premier). Mais vraiment, je pense que les cas où cela importera sont très artificiels.

Je verrais le problème plus du point de vue de savoir si vous voulez "encombrer les choses" avec beaucoup de classes anonymes. À titre indicatif, envisagez d'utiliser l'idiome pas plus que vous n'utiliseriez, par exemple, des classes anonymes pour les gestionnaires d'événements.

En (2), vous êtes à l'intérieur du constructeur d'un objet, donc "ceci" fait référence à l'objet que vous construisez. Ce n'est pas différent de tout autre constructeur.

Quant à (3), cela dépend vraiment de qui gère votre code, je suppose. Si vous ne le savez pas à l'avance, alors une référence que je suggérerais d'utiliser est "voyez-vous cela dans le code source du JDK?" (dans ce cas, je ne me souviens pas avoir vu de nombreux initialiseurs anonymes, et certainement pas dans les cas où c'est le seul contenu de la classe anonyme). Dans la plupart des projets de taille modérée, je dirais que vous aurez vraiment besoin que vos programmeurs comprennent la source JDK à un moment ou à un autre, donc toute syntaxe ou idiome utilisé est un "jeu équitable". Au-delà de cela, je dirais, formez les gens à cette syntaxe si vous avez le contrôle de qui gère le code, sinon commentez ou évitez.

Neil Coffey
la source
5

L'initialisation à double accolade est un hack inutile qui peut introduire des fuites de mémoire et d'autres problèmes

Il n'y a aucune raison légitime d'utiliser cette "astuce". Guava fournit de belles collections immuables qui incluent à la fois des usines statiques et des constructeurs, vous permettant de remplir votre collection là où elle est déclarée dans une syntaxe propre, lisible et sûre .

L'exemple dans la question devient:

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

Non seulement c'est plus court et plus facile à lire, mais cela évite les nombreux problèmes avec le modèle à double entretoise décrit dans d' autres réponses . Bien sûr, il fonctionne de manière similaire à un modèle directement construit HashMap, mais il est dangereux et sujet aux erreurs, et il existe de meilleures options.

Chaque fois que vous envisagez une initialisation à double croisillon, vous devez réexaminer vos API ou en introduire de nouvelles pour résoudre correctement le problème, plutôt que de profiter d'astuces syntaxiques.

Sujet aux erreurs marque désormais cet anti-modèle .

dimo414
la source
-1. Malgré quelques points valides, cette réponse se résume à "Comment éviter de générer des classes anonymes inutiles? Utilisez un framework, avec encore plus de classes!"
Agent_L
1
Je dirais que cela revient à "utiliser le bon outil pour le travail, plutôt qu'un hack qui peut planter votre application". La goyave est une bibliothèque assez courante pour les applications à inclure (vous manquez certainement si vous ne l'utilisez pas), mais même si vous ne voulez pas l'utiliser, vous pouvez et devez toujours éviter l'initialisation à double accolade.
dimo414
Et comment exactement une initialisation à double accolade provoquerait-elle une fuite de mémoire?
Angel O'Sphere
@ AngelO'Sphere DBI est un moyen obscur de créer une classe interne et conserve donc une référence implicite à sa classe englobante (sauf si elle n'est jamais utilisée dans des staticcontextes). Le lien sujet aux erreurs au bas de ma question en parle plus en détail.
dimo414
Je dirais que c'est une question de goût. Et il n'y a rien de vraiment obscur à ce sujet.
Angel O'Sphere
4

Je faisais des recherches sur cela et j'ai décidé de faire un test plus approfondi que celui fourni par la réponse valide.

Voici le code: https://gist.github.com/4368924

et c'est ma conclusion

J'ai été surpris de constater que dans la plupart des tests d'exécution, l'initiation interne était en fait plus rapide (presque le double dans certains cas). Lorsque vous travaillez avec de grands nombres, l'avantage semble s'estomper.

Il est intéressant de noter que le cas qui crée 3 objets sur la boucle perd ses avantages disparaît plus tôt que sur les autres cas. Je ne sais pas pourquoi cela se produit et d'autres tests devraient être effectués pour tirer des conclusions. La création d'implémentations concrètes peut aider à éviter de recharger la définition de classe (si c'est ce qui se passe)

Cependant, il est clair que peu de frais généraux ont été observés dans la plupart des cas pour le bâtiment à élément unique, même en grand nombre.

Un revers serait le fait que chacune des initiations à double accolade crée un nouveau fichier de classe qui ajoute un bloc de disque entier à la taille de notre application (ou environ 1k une fois compressé). Une petite empreinte, mais si elle est utilisée dans de nombreux endroits, elle pourrait potentiellement avoir un impact. Utilisez ceci 1000 fois et vous ajoutez potentiellement un Mio entier à votre application, ce qui peut être préoccupant dans un environnement embarqué.

Ma conclusion? Il peut être correct de l'utiliser tant qu'il n'est pas maltraité.

Laissez-moi savoir ce que vous pensez :)

pablisco
la source
2
Ce n'est pas un test valide. Le code crée des objets sans les utiliser, ce qui permet à l'optimiseur d'éluder la création entière de l'instance. Le seul effet secondaire restant est l'avancement de la séquence de nombres aléatoires dont les frais généraux l'emportent de toute façon sur ces tests.
Holger
3

J'appuie la réponse de Nat, sauf que j'utiliserais une boucle au lieu de créer et de lancer immédiatement la liste implicite de asList (éléments):

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
la source
1
Pourquoi? Le nouvel objet sera créé dans l'espace Eden et ne nécessite donc que deux ou trois ajouts de pointeur pour instancier. La machine virtuelle Java peut remarquer qu'elle ne s'échappe jamais au-delà de la portée de la méthode et donc l'allouer sur la pile.
Nat
Oui, il est susceptible de se révéler plus efficace que ce code (bien que vous puissiez l'améliorer en indiquant la HashSetcapacité suggérée - n'oubliez pas le facteur de charge).
Tom Hawtin - tackline
Eh bien, le constructeur HashSet doit quand même faire l'itération, donc ça ne sera pas moins efficace. Le code de bibliothèque créé pour la réutilisation doit toujours s'efforcer d'être le meilleur possible.
Lawrence Dol
3

Bien que cette syntaxe puisse être pratique, elle ajoute également une grande partie de ces références $ 0 au fur et à mesure qu'elles sont imbriquées et il peut être difficile d'intégrer le débogage dans les initialiseurs à moins que des points d'arrêt ne soient définis sur chacun d'eux. Pour cette raison, je recommande uniquement d'utiliser ceci pour les setters banaux, en particulier les constantes, et les endroits où les sous-classes anonymes n'ont pas d'importance (comme aucune sérialisation impliquée).

Eric Woodruff
la source
3

Mario Gleichman décrit comment utiliser les fonctions génériques Java 1.5 pour simuler les littéraux de la liste Scala, bien que vous vous retrouviez malheureusement avec des listes immuables .

Il définit cette classe:

package literal;

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

et l'utilise ainsi:

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, qui fait maintenant partie de Guava, prend en charge une idée similaire pour la construction de listes. Dans cette interview , Jared Levy dit:

[...] les fonctionnalités les plus utilisées, qui apparaissent dans presque toutes les classes Java que j'écris, sont des méthodes statiques qui réduisent le nombre de frappes répétitives dans votre code Java. C'est tellement pratique de pouvoir entrer des commandes comme les suivantes:

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

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

7/10/2014: Si seulement cela pouvait être aussi simple que celui de Python:

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

21/02/2020: En Java 11, vous pouvez maintenant dire:

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

Jim Ferrans
la source
2
  1. Cela appellera add()pour chaque membre. Si vous pouvez trouver un moyen plus efficace de placer des éléments dans un ensemble de hachage, utilisez-le. Notez que la classe interne générera probablement des ordures, si vous êtes sensible à cela.

  2. Il me semble que le contexte est l'objet renvoyé par new, qui est le HashSet.

  3. Si vous avez besoin de demander ... Plus probablement: les gens qui viennent après vous le savent-ils ou non? Est-il facile à comprendre et à expliquer? Si vous pouvez répondre «oui» aux deux, n'hésitez pas à l'utiliser.

Empereur MC
la source