758 votes

L'efficacité de Java "Double Croisillon d'Initialisation"?

Dans les Fonctions Cachées de Java le haut de réponse mentionne Double Croisillon d'Initialisation, avec une très séduisant syntaxe:

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

Cet idiome crée un anonyme intérieur de la classe avec juste un exemple de l'initialiseur, ce qui "peut utiliser n'importe quel [...] les méthodes dans le champ d'application".

Question principale: Est-ce aussi inefficace que ça sonne? Son utilisation soit limitée à des initialisations? (Et bien montrer!)

Deuxième question: Le new HashSet doit être le "ce" utilisée dans l'instance de l'initialiseur ... quelqu'un peut jeter de la lumière sur le mécanisme?

Troisième question: Est-ce un langage trop obscur à utiliser dans la production de code?

Résumé: très, Très gentil réponses, merci à tous. Sur la question (3), les gens ont senti la syntaxe devrait être clair (même si je recommande un commentaire occasionnel, surtout si votre code est de transmettre aux développeurs qui peuvent ne pas être familiers avec elle).

Sur la question (1), le code généré doit s'exécuter rapidement. L'extra .les fichiers de classe font du fichier jar de l'encombrement, et de ralentir le démarrage du programme légèrement (merci à @coobird pour la mesure). @Thilo a souligné que la collecte des ordures peut être affectée, et le coût mémoire pour le chargement des classes peut être un facteur dans certains cas.

Question (2) s'est avéré pour être le plus intéressant pour moi. Si j'ai bien compris les réponses, ce qui se passe dans DBI, c'est que l'anonyme intérieure classe étend la classe de l'objet en cours de construction par le nouvel opérateur, et a donc un "cette" valeur de référence l'instance en cours de construction. Très soigné.

Dans l'ensemble, DBI, me paraît être quelque chose d'une curiosité intellectuelle. Coobird et de l'autre, vous pouvez obtenir le même effet avec des Tableaux.asList, varargs méthodes, Google Collections, et le projet de Java 7 de la Collection de littéraux. Nouveaux JVM langues comme Scala, JRuby, et Groovy offrent aussi concis que des notations de construction de liste, et fonctionnent bien avec Java. Étant donné que DBI encombre jusqu'au classpath, ralentit le chargement des classes un peu, et rend le code un peu plus obscure, je serais probablement de le fuir. Cependant, j'ai prévu pour ce printemps sur un ami qui venait tout juste de son SCJP et aime la bonne humeur des joutes sur la sémantique de Java! ;-) Merci à tous!

571voto

coobird Points 70356

Mise à jour: Ajout d'une expérience pour évaluer la performance de la double accolade de l'initialisation.

Voici le problème quand je suis trop loin 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 les classes qui ont été générés, quand je faisais une application simple, et utilisé de grandes quantités d'anonymous inner classes: chaque classe seront compilées dans un distinct class le fichier.

Le "double croisillon d'initialisation", comme déjà mentionné, est un anonyme intérieur de la classe avec un exemple d'initialisation du bloc, ce qui signifie qu'une nouvelle classe est créée pour chaque "initialisation", tous dans le but de faire un objet unique.

Considérant que la Machine Virtuelle Java aurez besoin de lire toutes les classes lors de leur utilisation, qui peut conduire à un certain temps dans le bytecode de la vérification du processus et de ces. Pour ne pas mentionner l'augmentation de l'espace disque nécessaire pour stocker tous ces class fichiers.

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


Juste pour la référence, double croisillon d'initialisation est la suivante:

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

Il ressemble à un "caché" de Java, mais c'est juste une réécriture de:

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

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

C'est en fait une instance de l'initialisation du bloc qui fait partie d'un anonyme intérieur de la classe.


Comme une note ajoutée, si Joshua Bloch de Collecte des Littéraux de proposition pour le Projet de Pièce de monnaie de passe, nous pouvons être en mesure de voir ce genre de syntaxe dans Java 7:

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

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

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

Si ce changement rend à Java 7, il peut éliminer une bonne partie des cas d'utilisation pour la double accolade de l'initialisation.


Expérience

Voici l'expérience simple que j'ai testé-faire 1000 ArrayLists avec les éléments "Hello" et "World!" ajoutée par l'intermédiaire de l' add méthode, à l'aide de deux méthodes:

Méthode 1: Double Croisillon D'Initialisation

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

Méthode 2: Instancier un ArrayList et add

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

J'ai créé un programme simple d'écrire un fichier source de Java pour effectuer 1000 initialisation à l'aide de 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 nécessaire à l'initialisation de la 1000 ArrayLists et le 1000 anonyme classes internes s'étendant ArrayList est vérifiée à l'aide de l' System.currentTimeMillis, de sorte que 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 les 10 pistes des deux épreuves sont les suivantes:

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, le double croisillon initialisation d'un notable du temps d'exécution d'environ 190 ms.

Pendant ce temps, l' ArrayList initialisation de temps d'exécution est sorti à 0 ms. Bien sûr, la résolution du timer doit être pris en compte, mais il est susceptible d'être sous 15 ms secondes.

Donc, il semble y avoir une différence notable dans le temps d'exécution des deux méthodes. Il semble en effet qu'il existe une surcharge dans les deux méthodes d'initialisation.

Et oui, il y avait 1000 .class fichiers générés par la compilation de l' Test1 double accolade de l'initialisation du programme de test.

Enfin, je vous remercie pour la lecture de ce très longue réponse!

96voto

Thilo Points 108673

Une propriété de cette approche qui n'a pas été signalé jusqu'à présent est que parce que vous créez les classes internes, l'ensemble de la classe conteneur est capturé dans son champ d'application. Cela signifie que tant que votre Jeu est en vie, il conserve un pointeur vers l'instance (this$0) et de garder que de le garbage collector, qui pourrait être un problème.

Cela, et le fait qu'une nouvelle classe est créée, en premier lieu, même si régulièrement HashSet serait fonctionnent tout aussi bien (ou même mieux), fait que je ne veux pas utiliser cette construction (même si j'ai vraiment long pour le sucre syntaxique).

Deuxième question: Le new HashSet doit être le "ce" utilisée dans l'instance de l'initialiseur ... quelqu'un peut jeter de la lumière sur le mécanisme? J'aurais naïvement prévu "ce" pour se référer à l'objet de l'initialisation de "saveurs".

Ce est juste la façon dont les classes internes de travail. Qu'ils obtiennent leur propre this, mais ils ont aussi des pointeurs vers le parent de l'instance, de sorte que vous pouvez appeler des méthodes sur l'objet contenant. Dans le cas d'un conflit de noms, à l'intérieur de la classe (dans votre cas, HashSet) prend le pas, mais vous pouvez le préfixe "ce" avec un nom de classe pour obtenir la méthode extérieure.

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 l'anonyme sous-classe en cours de création, vous pouvez définir des méthodes de là aussi bien. Par exemple remplacer HashSet.add()

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

            @Override
            boolean add(String s){

            }

        };
    }

35voto

Eddie Points 27755

Prenant la suite de la classe de test:

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

et 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 n'a pas l'air terriblement inefficace pour moi. Si j'étais inquiet au sujet de la performance pour quelque chose comme cela, j'avais le profil. Et votre question n ° 2 est répondu par le code ci-dessus: Vous êtes à l'intérieur d'un constructeur implicite (l'instance et de l'initialiseur) pour l'intérieur de votre classe,"this" désigne le présent intérieure de la classe.

Oui, cette syntaxe est obscure, mais un commentaire peut clarifier obscur de la syntaxe d'utilisation. Pour clarifier la syntaxe, la plupart des gens sont familiers avec un initialiseur statique bloc (JLS 8.7 Initialiseurs Statiques):

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 le constructeur d'utilisation (JLS 8.6 Exemple les Initialiseurs), bien que je n'ai jamais vu cela 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 un constructeur par le compilateur. Avec cela à l'esprit, à démêler la double croisillon code:

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

Le bloc de code entre à l'intérieur de la plupart des accolades est transformé en un constructeur par le compilateur. L'extérieur-la plupart des accolades délimiter l'anonyme intérieur de la classe. Pour profiter de cette la dernière étape de faire tout ce 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");
    }
}

Pour l'initialisation, je dirais qu'il y a pas de frais généraux que ce soit (ou si petite qu'elle peut être négligée). Cependant, chaque utilisation d' flavors iront pas à l'encontre HashSet mais plutôt à l'encontre MyHashSet. Il y a probablement une petite (et très probablement négligeable de charge). Mais encore une fois, avant que je inquiet à ce sujet, je voudrais profil.

Encore une fois, à votre question #2, le code ci-dessus est la logique et explicite équivalent de la double accolade de l'initialisation, et il le fait évident où "this" fait référence: À l'intérieur de la classe qui étend la classe HashSet.

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

35voto

bestsss Points 6403

fuite sujettes

J'ai décidé de carillon. L'impact de la performance comprend: opération de disque + décompresser (jar), la classe de vérification, perm-gen de l'espace (pour le Soleil de la JVM Hotspot). Cependant, le pire de tous: c'est une fuite sur le ventre. Vous ne pouvez pas il suffit de le retourner.

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

Donc, si l'ensemble échappe à toute autre partie chargée par un chargeur de classes différentes et une référence y est conservé, l'ensemble de l'arborescence de classes+chargeur de classe sera coulé. Pour éviter cela, une copie de HashMap est nécessaire, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}}). Pas très joli, pas plus. Je n'utilise pas le langage, moi-même, c'est plutôt comme new LinkedHashSet(Arrays.asList("xxx","YYY"));

19voto

Peter Lawrey Points 229686

Chargement d'un grand nombre de classes peut ajouter quelques millisecondes pour le départ. Si le démarrage n'est pas 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();
    }
}

imprime

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by: