Java“双支撑初始化”的效率?
问题
InHidden Features of Java最佳答案提及Double Brace Initialization,具有非常的语法:
Set<String> flavors = new HashSet<String>() {{
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}};
这个成语创建了一个匿名内部类,其中只包含一个实例初始化程序,“可以使用包含作用域中的任何[...]方法”。
主要问题:这听起来像as效率低吗吗?它的使用是否应限于一次性初始化? (当然炫耀!)
第二个问题:新的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:Baeldunghas a good summary双支撑初始化并认为它是一种反模式。
12/2017:@Basil Bourque指出,在新的Java 9中你可以说:
Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");
这肯定是要走的路。如果你遇到早期版本,请查看Google Collections' ImmutableSet。
#1 热门回答(524 赞)
当我被匿名的内部类带走时,这就是问题所在:
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虚拟机在使用它们时需要读取所有这些类,这可能会导致bytecode verfication进程中的某些时间等。更不用说为了存储所有那些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!");
}
};
所以它基本上是ainstance initialization block,它是anonymous inner class的一部分。
Joshua Bloch'sCollection Literals proposalforProject Coin的主旨是:
List<Integer> intList = [1, 2, 3, 4];
Set<String> strSet = {"Apple", "Banana", "Cactus"};
Map<String, Integer> truthMap = { "answer" : 42 };
可悲的是,didn't make its way既没有Java 7也没有8并被无限期搁置。
实验
这是我测试过的简单实验 - 使用以下两种方法,通过add
方法将“!Hello”和“World!”添加到1000!ArrayList`s中。
方法1:Double Brace Initialization
List<String> l = new ArrayList<String>() {{
add("Hello");
add("World!");
}};
Method 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
检查初始化1000ArrayList的经过时间和扩展
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
可以看出,双支撑初始化具有大约190毫秒的显着执行时间。
同时,ArrayList
初始化执行时间为0毫秒。当然,应该考虑定时器分辨率,但它可能不到15毫秒。
因此,两种方法的执行时间似乎有明显的差异。看起来两个初始化方法确实存在一些开销。
是的,通过编译Test1
双括号初始化测试程序生成了1000个.class
文件。
#2 热门回答(89 赞)
到目前为止尚未指出此方法的一个属性是因为你创建内部类,所以在其范围内捕获整个包含类。这意味着只要你的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){
}
};
}
#3 热门回答(34 赞)
参加以下测试课程:
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静态初始化器):
public class Sample1 {
private static final String someVar;
static {
String temp = null;
..... // block of code setting temp
someVar = temp;
}
}
你也可以使用类似的语法(没有“静态”一词)来构造函数使用(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");
}
}
出于初始化的目的,我会说没有任何开销(或者很小以至于可以忽略)。但是,每次使用flavors
都不会反对HashSet
而是反对MyHashSet
。这可能是一个很小的(很可能是微不足道的)开销。但是,在我担心之前,我会再说一下。
同样,对于你的问题#2,上面的代码是双括号初始化的逻辑和显式等价物,它使得“this
”指的是明显的:扩展HashSet
的内部类。
如果你对实例初始化程序的详细信息有疑问,请查看JLS文档中的详细信息。