在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。


当前回答

双大括号初始化是一种不必要的攻击,可能会导致内存泄漏和其他问题

没有合理的理由使用这种“伎俩”。Guava提供了很好的不可变集合,包括静态工厂和构建器,允许您在以干净、可读和安全的语法声明的地方填充集合。

问题中的例子是:

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

这不仅更短,更容易阅读,而且避免了其他答案中描述的双括号模式的许多问题。当然,它的性能类似于直接构造的HashMap,但它很危险且容易出错,而且还有更好的选择。

任何时候,当你发现自己在考虑双括号初始化时,你都应该重新检查你的api或引入新的api来正确地解决这个问题,而不是利用语法技巧。

Error-Prone现在标记此反模式。

其他回答

双大括号初始化是一种不必要的攻击,可能会导致内存泄漏和其他问题

没有合理的理由使用这种“伎俩”。Guava提供了很好的不可变集合,包括静态工厂和构建器,允许您在以干净、可读和安全的语法声明的地方填充集合。

问题中的例子是:

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

这不仅更短,更容易阅读,而且避免了其他答案中描述的双括号模式的许多问题。当然,它的性能类似于直接构造的HashMap,但它很危险且容易出错,而且还有更好的选择。

任何时候,当你发现自己在考虑双括号初始化时,你都应该重新检查你的api或引入新的api来正确地解决这个问题,而不是利用语法技巧。

Error-Prone现在标记此反模式。

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

代码如下: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.

让我知道你的想法:)

要创建集合,你可以使用varargs工厂方法来代替双大括号初始化:

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

谷歌Collections库有很多这样的方便方法,以及大量其他有用的功能。

至于这个习语的晦涩性,我一直在产品代码中遇到并使用它。我更关心的是那些被允许编写产品代码的习惯用法所迷惑的程序员。

我第二Nat的答案,除了我将使用一个循环,而不是创建并立即从asList(elements)中抛出隐式列表:

static public Set<T> setOf(T ... elements) {
    Set set=new HashSet<T>(elements.size());
    for(T elm: elements) { set.add(elm); }
    return set;
    }

到目前为止还没有指出的这种方法的一个特性是,因为创建了内部类,所以整个包含类都在其作用域中被捕获。这意味着只要你的Set是活的,它将保留一个指向包含实例的指针(这个$0),并防止它被垃圾收集,这可能是一个问题。

这一点,以及尽管常规HashSet可以很好地工作(甚至更好),但首先创建了一个新类的事实,使我不想使用这个构造(尽管我非常渴望语法糖)。

第二个问题:新的HashSet必须是实例初始化器中使用的“this”…有人能解释一下原理吗?我天真地以为“this”指的是初始化“flavors”的对象。

这就是内部类的工作方式。它们有自己的this,但是它们也有指向父实例的指针,所以你也可以调用包含对象的方法。在命名冲突的情况下,内部类(在您的例子中是HashSet)优先,但是您可以在类名前面加上“this”来获得外部方法。

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

为了清楚地了解正在创建的匿名子类,您也可以在其中定义方法。例如重写HashSet.add()

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

            @Override
            boolean add(String s){

            }

        };
    }