在Java的隐藏功能中 ,最佳答案提到了Double Brace Initialization ,它具有非常诱人的语法:
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” 值。井井有条。
总的来说,DBI 让我感到非常好奇。 Coobird 和其他人指出,您可以使用 Arrays.asList,varargs 方法,Google Collections 和提议的 Java 7 Collection 文字获得相同的效果。 Scala,JRuby 和 Groovy 等较新的 JVM 语言也为列表构建提供了简明的符号,并且与 Java 良好地互操作。鉴于 DBI 使类路径混乱,减慢了类加载速度,并使代码更加模糊,我可能会回避它。但是,我打算在一位刚刚获得 SCJP 的朋友身上发表这篇文章,并且喜欢关于 Java 语义的好朋友! ;-) 感谢大家!
7/2017:Baeldung 对双支撑初始化有很好的总结 ,并认为它是一种反模式。
12/2017:@Basil Bourque 指出,在新的 Java 9 中你可以说:
Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");
这肯定是要走的路。如果您遇到早期版本,请查看Google Collections 的 ImmutableSet 。
当我被匿名的内部类带走时,这就是问题所在:
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
这些都是在我创建一个简单的应用程序时生成的类,并使用了大量的匿名内部类 - 每个类都将被编译成一个单独的class
文件。
如前所述,“双括号初始化” 是一个带有实例初始化块的匿名内部类,这意味着为每个 “初始化” 创建一个新类,所有这些都是为了通常制作单个对象。
考虑到 Java 虚拟机在使用它们时需要读取所有这些类,这可能会导致字节码验证过程中出现一些时间等。更不用说为了存储所有这些class
文件而增加所需的磁盘空间。
在使用双支撑初始化时似乎有一些开销,所以过分使用它可能不是一个好主意。但正如埃迪在评论中指出的那样,不可能完全确定其影响。
仅供参考,双支撑初始化如下:
List<String> list = new ArrayList<String>() {{
add("Hello");
add("World!");
}};
它看起来像 Java 的 “隐藏” 功能,但它只是重写:
List<String> list = new ArrayList<String>() {
// Instance initialization block
{
add("Hello");
add("World!");
}
};
所以它基本上是一个实例初始化块 ,它是匿名内部类的一部分 。
Joshua Bloch 关于Project Coin的Collection Literals 提案遵循以下方针:
List<Integer> intList = [1, 2, 3, 4];
Set<String> strSet = {"Apple", "Banana", "Cactus"};
Map<String, Integer> truthMap = { "answer" : 42 };
可悲的是,它没有进入 Java 7 和 8 并且无限期搁置。
实验
这是我测试过的简单实验 - 使用"Hello"
和"World!"
元素制作 1000 个ArrayList
。使用以下两种方法通过add
方法添加到它们:
方法 1:双支撑初始化
List<String> l = new ArrayList<String>() {{
add("Hello");
add("World!");
}};
方法 2:实例化ArrayList
并add
List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");
我创建了一个简单的程序来编写 Java 源文件,使用这两种方法执行 1000 次初始化:
测试 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);
}
}
测试 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);
}
}
请注意,使用System.currentTimeMillis
检查初始化 1000 ArrayList
和扩展ArrayList
的 1000 个匿名内部类所用的时间,因此计时器的分辨率不是很高。在我的 Windows 系统上,分辨率大约为 15-16 毫秒。
两次测试的 10 次运行的结果如下:
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
可以看出,双支撑初始化具有大约 190ms 的显着执行时间。
同时, ArrayList
初始化执行时间为 0 毫秒。当然,应该考虑定时器分辨率,但它可能不到 15 毫秒。
因此,两种方法的执行时间似乎有明显的差异。看起来两个初始化方法确实存在一些开销。
是的,通过编译Test1
双括号初始化测试程序生成了 1000 个.class
文件。
到目前为止尚未指出此方法的一个属性是因为您创建内部类,所以在其范围内捕获整个包含类。这意味着只要你的 Set 处于活动状态,它就会保留一个指向包含实例的指针( this$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){
}
};
}
每当有人使用双支撑初始化时,一只小猫就会被杀死。
除了语法相当不寻常而且不是真正的惯用语(当然,品味是值得商榷的)之外,您在应用程序中不必要地创建了两个重要问题, 我最近在这里更详细地讨论了这些问题 。
每次使用双括号初始化时,都会生成一个新类。例如这个例子:
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");
}});
}});
}};
... 将产生这些类:
Test$1$1$1.class
Test$1$1$2.class
Test$1$1.class
Test$1.class
Test.class
这对你的类加载器来说是一个相当大的开销 - 什么都不是!当然,如果你这样做一次,它将不需要太多的初始化时间。但是,如果你在整个企业应用程序中执行此操作 20,000 次... 所有堆内存只是为了一点 “语法糖”?
如果您使用上面的代码并从方法返回该映射,那么该方法的调用者可能会毫无疑问地保留非常繁重的资源,而这些资源无法进行垃圾回收。请考虑以下示例:
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;
}
}
返回的Map
现在将包含对ReallyHeavyObject
的封闭实例的ReallyHeavyObject
。你可能不想冒这样的风险:
图片来自http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/
为了回答你的实际问题,人们一直在使用这种语法假装 Java 有类似于现有数组文字的地图文字:
String[] array = { "John", "Doe" };
Map map = new HashMap() {{ put("John", "Doe"); }};
有些人可能会发现这种语法刺激。