在Java的隐藏特性中,最上面的答案提到了双大括号初始化,语法非常诱人:

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

这个习惯用法创建了一个匿名的内部类,其中只有一个实例初始化式,它“可以使用任何[…]]方法”。

主要问题:这真的像听起来那么低效吗?它的使用应该仅限于一次性的初始化吗?(当然还有炫耀!)

第二个问题:新的HashSet必须是实例初始化器中使用的“this”…有人能解释一下原理吗?

第三个问题:在产品代码中使用这个习语是否过于晦涩?

非常非常好的回答,谢谢大家。关于问题(3),人们认为语法应该清楚(尽管我建议偶尔注释一下,特别是如果您的代码将传递给可能不熟悉它的开发人员)。

对于问题(1),生成的代码应该快速运行。额外的.class文件确实会导致jar文件混乱,并略微减慢程序启动速度(感谢@coobird测量)。@Thilo指出,垃圾收集可能会受到影响,在某些情况下,额外加载类的内存成本可能是一个因素。

问题(2)对我来说是最有趣的。如果我理解了答案,DBI中发生的事情是匿名内部类扩展了new操作符所构造的对象的类,因此有一个“this”值引用正在构造的实例。非常整洁。

Overall, DBI strikes me as something of an intellectual curiousity. Coobird and others point out you can achieve the same effect with Arrays.asList, varargs methods, Google Collections, and the proposed Java 7 Collection literals. Newer JVM languages like Scala, JRuby, and Groovy also offer concise notations for list construction, and interoperate well with Java. Given that DBI clutters up the classpath, slows down class loading a bit, and makes the code a tad more obscure, I'd probably shy away from it. However, I plan to spring this on a friend who's just gotten his SCJP and loves good natured jousts about Java semantics! ;-) Thanks everyone!

7/2017: Baeldung很好地总结了双大括号初始化,并认为这是一种反模式。

2017年12月12日:@Basil Bourque指出,在新的Java 9中,你可以说:

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

这肯定是正确的选择。如果你还在使用以前的版本,可以看看谷歌Collections的ImmutableSet。


当前回答

我正在研究这个问题,并决定做一个比有效答案提供的更深入的测试。

代码如下:https://gist.github.com/4368924

这就是我的结论

I was surprised to find that in most of the run tests the internal initiation was actually faster (almost double in some cases). When working with large numbers the benefit seems to fade away. Interestingly, the case that creates 3 objects on the loop loses it's benefit rans out sooner than on the other cases. I am not sure why this is happening and more testing should be done to reach any conclusions. Creating concrete implementations may help to avoid the class definition to be reloaded (if that's what's happening) However, it is clear that not much overhead it observed in most cases for the single item building, even with large numbers. One set back would be the fact that each of the double brace initiations creates a new class file that adds a whole disk block to the size of our application (or about 1k when compressed). A small footprint, but if it's used in many places it could potentially have an impact. Use this 1000 times and you are potentially adding a whole MiB to you applicaiton, which may be concerning on an embedded environment. My conclusion? It can be ok to use as long as it is not abused.

让我知道你的想法:)

其他回答

加载许多类会在开始时增加几毫秒的时间。如果启动不是那么关键,你看看类启动后的效率,没有区别。

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

打印

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

容易泄漏

我决定插话了。对性能的影响包括:磁盘操作+解压缩(针对jar)、类验证、烫发空间(针对Sun的Hotspot JVM)。 然而,最糟糕的是:它容易泄漏。你不能简单地回去。

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

因此,如果集合转义到由不同的类加载器加载的任何其他部分,并且引用保留在那里,则整个类+类加载器树将被泄漏。为了避免这种情况,需要复制HashMap, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}})。不再那么可爱了。 我自己不使用成语,而是像new LinkedHashSet(Arrays.asList("xxx","YYY"));

执行以下测试类:

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

然后反编译类文件,我看到:

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

在我看来,这并不是非常低效。如果我担心这样的东西的性能,我会分析它。上面的代码回答了你的问题#2:你在内部类的隐式构造函数(和实例初始化式)中,所以“this”指的是这个内部类。

是的,这个语法是模糊的,但是注释可以阐明模糊的语法用法。为了澄清语法,大多数人都熟悉静态初始化块(JLS 8.7 static Initializers):

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

您还可以使用类似的语法(没有“static”这个词)来使用构造函数(JLS 8.6实例初始化器),尽管我从未在生产代码中看到过这种用法。这一点很少为人所知。

public class Sample2 {
    private final String someVar;

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

如果没有默认构造函数,则编译器将{和}之间的代码块转换为构造函数。考虑到这一点,解开双括号代码:

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

最里面的大括号之间的代码块被编译器转换为构造函数。最外面的花括号分隔了匿名内部类。这是使一切都非匿名的最后一步:

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

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

对于初始化的目的,我认为没有任何开销(或者小到可以忽略不计)。但是,每次使用flavor都不会违背HashSet,而是违背MyHashSet。这可能有一个很小的开销(很可能可以忽略不计)。但是,在我担心之前,我会先分析一下。

同样,对于您的问题#2,上面的代码在逻辑上和显式地等价于双大括号初始化,并且它使“this”指的地方很明显:指向扩展HashSet的内部类。

如果您对实例初始化器的详细信息有疑问,请查看JLS文档中的详细信息。

我正在研究这个问题,并决定做一个比有效答案提供的更深入的测试。

代码如下:https://gist.github.com/4368924

这就是我的结论

I was surprised to find that in most of the run tests the internal initiation was actually faster (almost double in some cases). When working with large numbers the benefit seems to fade away. Interestingly, the case that creates 3 objects on the loop loses it's benefit rans out sooner than on the other cases. I am not sure why this is happening and more testing should be done to reach any conclusions. Creating concrete implementations may help to avoid the class definition to be reloaded (if that's what's happening) However, it is clear that not much overhead it observed in most cases for the single item building, even with large numbers. One set back would be the fact that each of the double brace initiations creates a new class file that adds a whole disk block to the size of our application (or about 1k when compressed). A small footprint, but if it's used in many places it could potentially have an impact. Use this 1000 times and you are potentially adding a whole MiB to you applicaiton, which may be concerning on an embedded environment. My conclusion? It can be ok to use as long as it is not abused.

让我知道你的想法:)

这将为每个成员调用add()。如果你能找到一种更有效的方法将项放入散列集中,那么就使用它。注意,内部类可能会生成垃圾,如果您对此很敏感的话。 在我看来,上下文似乎是new返回的对象,也就是HashSet。 如果你需要问…更有可能的是:在你之后的人会知道这一点吗?它容易理解和解释吗?如果两个问题你都能回答“是”,那就随便用吧。