在Hidden Features of Java中,最佳答案提到Double Brace Initialization,语法非常诱人:
Set<String> flavors = new HashSet<String>() {{
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}};
这个成语创建了一个匿名内部类,其中只包含一个实例初始化程序,“可以使用包含作用域中的任何[...]方法” .
主要问题:听起来是 inefficient 吗?它的使用是否应限于一次性初始化? (当然炫耀!)
第二个问题:新的HashSet必须是实例初始化程序中使用的“this”...任何人都可以阐明机制吗?
第三个问题:这个成语是否也在 生产环境 代码中使用 obscure ?
Summary: 非常非常好的答案,谢谢大家 . 在问题(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 has a good summary的双括号初始化并认为它是反模式 .
12/2017:@Basil Bourque指出,在新的Java 9中你可以说:
Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");
那个's for sure the way to go. If you'坚持早期版本,看看Google Collections' ImmutableSet .
15 回答
我第二个Nat的答案,除了我会使用循环而不是创建并立即从asList(元素)抛出隐式List:
leak prone
我决定加入进来 . 性能影响包括:磁盘操作解压缩(用于jar),类验证,perm-gen空间(用于Sun的Hotspot JVM) . 然而,最糟糕的是:它容易发生泄漏 . 你不能简单地回来 .
因此,如果集合转义到由不同类加载器加载的任何其他部分并且引用保留在那里,那么类classloader的整个树将被泄露 . 为避免这种情况,需要复制到HashMap,
new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}})
. 不再那么可爱了 . 我不使用这个成语,我自己,而不是像new LinkedHashSet(Arrays.asList("xxx","YYY"));
这将为每个成员调用
add()
. 如果您可以找到更有效的方法将项目放入哈希集,那么使用它 . 请注意,如果您对此敏感,内部类可能会生成垃圾 .在我看来,好像上下文是
new
返回的对象,即HashSet
.如果你需要问...更有可能:跟你们相关的人会不会知道?理解和解释容易吗?如果您对两者都回答“是”,请随意使用它 .
除了效率之外,我很少发现自己希望在单元测试之外创建声明性集合 . 我确实认为双括号语法非常易读 .
另一种实现列表声明性构造的方法是使用
Arrays.asList(T ...)
,如下所示:此方法的局限性当然是您无法控制要生成的特定类型的列表 .
到目前为止尚未指出此方法的一个属性是因为您创建内部类,所以在其范围内捕获整个包含类 . 这意味着只要你的Set处于活动状态,它就会保留一个指向包含实例(
this$0
)的指针,并防止它被垃圾收集,这可能是一个问题 .这个,以及即使常规HashSet工作得很好(甚至更好)也能在第一时间创建新类的事实,这使得我不想使用这个构造(即使我真的渴望语法糖) .
这就是内部类的工作方式 . 它们有自己的
this
,但它们也有指向父实例的指针,因此您也可以调用包含对象的方法 . 在在命名冲突的情况下,内部类(在您的情况下为HashSet)优先,但您可以使用类名前缀"this"来获取外部方法 .要明确创建的匿名子类,您也可以在其中定义方法 . 例如覆盖
HashSet.add()
Every time someone uses double brace initialisation, a kitten gets killed.
除了语法相当不寻常而且不是真正的惯用语(当然味道有争议)之外,您在应用程序中不必要地创建了两个重要问题,which I've just recently blogged about in more detail here .
1.你正在创建太多的匿名类
每次使用双括号初始化时,都会生成一个新类 . 例如 . 这个例子:
...将产生这些类:
这对你的类加载器来说是一个相当大的开销 - 什么都不是!当然,如果你这样做一次,它将不需要太多的初始化时间 . 但是,如果你在整个企业应用程序中执行此操作20'000次...所有堆内存只是为了一点“语法糖”?
2.您可能会造成内存泄漏!
如果您使用上面的代码并从方法返回该映射,那么该方法的调用者可能会毫无疑问地保留非常繁重的资源,而这些资源无法进行垃圾回收 . 请考虑以下示例:
返回的
Map
现在将包含对ReallyHeavyObject
的封闭实例的引用 . 你可能不想冒这个风险:图片来自http://blog.jooq.org/2014/12/08/dont-be-clever-the-double-curly-braces-anti-pattern/
3.您可以假装Java具有 Map 文字
为了回答你的实际问题,人们一直在使用这种语法假装Java有类似于现有数组文字的 Map 文字:
有些人可能会发现这种语法刺激 .
我正在研究这个问题,并决定进行比有效答案提供的更深入的测试 .
这是代码:https://gist.github.com/4368924
这是我的结论
让我知道你的想法 :)
通常没什么特别低效的 . 你已经创建了一个子类并为它添加了一个构造函数对JVM来说通常并不重要 - 这是一种在面向对象语言中常常做的正常事情 . 我可以想到一些非常人为的例子,你可能会因此而导致效率低下(例如,你有一个反复调用的方法,由于这个子类而最终会混合使用不同的类,而传入的类通常是完全可预测的 - - 在后一种情况下,JIT编译器可以进行首先不可行的优化 . 但实际上,我认为重要的案例是非常人为的 .
从你是否想要用大量匿名类“混乱”的角度来看,我会更多地看到这个问题 . 作为一个粗略的指南,请考虑使用成语,而不是使用事件处理程序的匿名类 .
在(2)中,您位于对象的构造函数中,因此“this”指的是您正在构造的对象 . 这与任何其他构造函数没有什么不同 .
至于(3),这实际上取决于谁提前知道这一点,那么我建议使用的基准是"do you see this in the source code to the JDK?"(在这种情况下,我不是匿名类的唯一内容) . 在大多数中等规模的项目中,我真的需要你的程序员在某些时候理解JDK源代码,所以在那里使用的语法或习语是"fair game" . 除此之外,我维护代码,否则评论或避免 .
双支撑初始化是一种不必要的破解,可能会引入内存泄漏和其他问题
使用这个"trick"是没有正当理由的 . Guava提供了很好的immutable collections,其中包括静态工厂和构建器,允许您在清晰,可读和安全的地方填充您的集合 . 句法 .
问题中的示例变为:
这不仅更短,更容易阅读,而且避免了other answers中描述的双支撑模式的众多问题 . 当然,它的表现与直接构造的
HashMap
类似,但它很危险且容易出错,并且有更好的选择 .每当你发现自己考虑双括号初始化时,你应该重新检查你的API或introduce new ones以正确解决问题,而不是利用语法技巧 .
Error-Prone现在flags this anti-pattern .
要创建集合,您可以使用varargs工厂方法而不是双支撑初始化:
Google Collections库有许多这样的便捷方法,以及许多其他有用的功能 .
至于成语的默默无闻,我遇到它并一直在 生产环境 代码中使用它 . 我更关心那些被允许编写 生产环境 代码的习语感到困惑的程序员 .
当我被匿名的内部类带走时,这就是问题所在:
这些都是在我创建一个简单的应用程序时生成的类,并使用了大量的匿名内部类 - 每个类都将被编译成一个单独的
class
文件 .如前所述,“双括号初始化”是一个带有实例初始化块的匿名内部类,这意味着为每个“初始化”创建一个新类,所有这些都是为了生成单个对象 .
考虑到Java虚拟机在使用它们时需要读取所有这些类,这可能会导致bytecode verfication进程中的某些时间等 . 更不用说为了存储所有那些
class
文件而增加所需的磁盘空间 .在使用双支撑初始化时似乎有一些开销,所以用它过于夸张可能不是一个好主意 . 但正如埃迪在评论中指出的那样,不可能完全确定其影响 .
仅供参考,双支撑初始化如下:
它看起来像Java的“隐藏”功能,但它只是重写:
所以它基本上是instance initialization block,它是anonymous inner class的一部分 .
Joshua Bloch的Collection Literals proposal为Project Coin是沿着以下方向:
可悲的是,它既不是Java 7也不是8,并且被无限期搁置 .
Experiment
这里's the simple experiment I'已经测试过 - 使用两种方法,通过
add
方法将"Hello"
和"World!"
元素添加到1000ArrayList
中:方法1:双支撑初始化
方法2:实例化
ArrayList
和add
我创建了一个简单的程序来编写Java源文件,使用这两种方法执行1000次初始化:
测试1:
测试2:
请注意,使用
System.currentTimeMillis
检查初始化1000ArrayList
s和扩展ArrayList
的1000个匿名内部类所用的时间,因此计时器的分辨率不是很高 . 在我的Windows系统上,分辨率大约为15-16毫秒 .两次测试的10次运行的结果如下:
可以看出,双支撑初始化具有大约190毫秒的显着执行时间 .
同时,
ArrayList
初始化执行时间为0 ms . 当然,应该考虑定时器分辨率,但它可能不到15毫秒 .因此,两种方法的执行时间似乎有明显的差异 . 看起来两个初始化方法确实存在一些开销 .
是的,通过编译
Test1
双括号初始化测试程序生成了1000个.class
文件 .Mario Gleichman describes如何使用Java 1.5泛型函数来模拟Scala List文字,尽管遗憾的是你最终使用了不可变列表 .
他定义了这个类:
并因此使用它:
Google Collections现在是Guava的一部分,它支持列表构建的类似想法 . 在this interview,Jared Levy说:
2014年7月10日:如果它只是像Python那样简单:
参加以下测试课程:
然后反编译该类文件,我看到:
这不是't look terribly inefficient to me. If I were worried about performance for something like this, I' d . 并且您的问题#2由上面的代码回答:您在内部类的隐式构造函数(和实例初始化程序)中,因此“
this
”指的是此内部类 .是的,这种语法很模糊,但注释可以澄清模糊的语法用法 . 为了澄清语法,大多数人都熟悉静态初始化程序块(JLS 8.7静态初始化程序):
您也可以使用类似的语法(没有单词“
static
”)来构造函数使用(JLS 8.6实例初始化程序),尽管我从未在 生产环境 代码中看到过这种情况 . 这种情况不太为人所知 .如果您没有默认构造函数,则编译器会将
{
和}
之间的代码块转换为构造函数 . 考虑到这一点,解开双括号代码:最内部大括号之间的代码块由编译器转换为构造函数 . 最外面的大括号分隔匿名内部类 . 要把这一切变成非匿名的最后一步:
出于初始化的目的,我会说没有任何开销(或者很小,以至于可以忽略) . 但是,每次使用
flavors
都不会违反HashSet
而是反对MyHashSet
. 这可能是一个很小的(很可能是微不足道的)开销 . 但是,在我担心之前,我会再说一下 .同样,对于你的问题#2,上面的代码是双支撑初始化的逻辑和显式等价,它使“
this
”指的是明显的:对于扩展HashSet
的内部类 .如果您对实例初始化程序的详细信息有疑问,请查看JLS文档中的详细信息 .
虽然这种语法很方便,但它也会添加很多这个$ 0引用,因为这些引用会嵌套,除非在每个引用上设置断点,否则很难将步骤调试到初始化器中 . 出于这个原因,我只建议将它用于平庸的setter,特别是设置为常量,以及匿名子类无关紧要的地方(比如没有涉及序列化) .
加载许多类可以在开始时添加几毫秒 . 如果启动不那么重要,而且你在启动后看看类的效率,则没有区别 .
版画