Java“双Brace初始化”的效率?
在Java的隐藏特性中 ,最佳答案提到了Double Brace Initialization ,其语法非常诱人:
Set<String> flavors = new HashSet<String>() {{ add("vanilla"); add("strawberry"); add("chocolate"); add("butter pecan"); }};
这个习语创build了一个匿名的内部类,只有一个实例初始化器,它可以使用“包含范围内的任何方法”。
主要问题:这听起来效率不高吗? 它的使用应该限于一次性的初始化吗? (当然,炫耀!)
第二个问题:新的HashSet必须是在实例初始化器中使用的“this”…任何人都可以阐明机制?
第三个问题:这个习语在生产代码中是不是太模糊了?
总结:非常非常好的答案,谢谢大家。 在问题(3)中,人们认为语法应该是清楚的(尽pipe我会build议偶然的评论,特别是如果你的代码会传递给可能不熟悉的开发人员)。
在问题(1)中,生成的代码应该快速运行。 额外的.class文件确实会导致jar文件混乱,并且程序启动稍微慢一些(感谢@coobird的测量)。 @Thilo指出,垃圾收集可能会受到影响,并且额外加载的类的内存成本在某些情况下可能是一个因素。
问题(2)是我最感兴趣的。 如果我理解了答案,那么DBI中发生的事情就是匿名内部类扩展了由new运算符构造的对象的类,因此具有引用正在构造的实例的“this”值。 井井有条。
总的来说,DBI让我觉得自己是一个知识分子的好奇心。 Coobird和其他人指出,你可以使用Arrays.asList,可变参数方法,Google Collections和build议的Java 7集合文字实现相同的效果。 像Scala,JRuby和Groovy等更新的JVM语言也为列表构build提供简洁的符号,并与Java良好的互操作性。 鉴于DBI混乱了类path,减慢了类的加载速度,并使代码更加隐晦,我可能会避开它。 不过,我打算在一位刚刚获得SCJP的朋友面前谈论这个问题,并且热衷于讨论Java语义。 ;-) 感谢大家!
7/2017:Baeldung对双支撑初始化有一个很好的总结 ,并认为它是一种反模式。
当我忘记匿名内部类时,问题就出现了:
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
文件。
正如已经提到的那样,“双大括号初始化”是一个带有实例初始化块的匿名内部类,这意味着为每个“初始化”创build一个新类,所有这些都是为了通常制作单个对象。
考虑到Java虚拟机在使用时需要读取所有这些类,这可能会导致在字节码validation过程中出现一些时间等等。 更不用说增加所需的磁盘空间来存储所有这些class
文件。
在使用双括号初始化的时候好像有一点点的开销,所以可能不是一个好主意。 但正如埃迪在评论中指出的那样,不可能绝对确定这种影响。
仅供参考,双括号初始化如下:
List<String> list = new ArrayList<String>() {{ add("Hello"); add("World!"); }};
它看起来像Java的“隐藏”function,但它只是一个重写:
List<String> list = new ArrayList<String>() { // Instance initialization block { add("Hello"); add("World!"); } };
所以它基本上是一个实例初始化块 ,是匿名内部类的一部分 。
约书亚·布洛赫的文集关于 “ 项目硬币”的 字面意思是:
List<Integer> intList = [1, 2, 3, 4]; Set<String> strSet = {"Apple", "Banana", "Cactus"}; Map<String, Integer> truthMap = { "answer" : 42 };
可悲的是,它并没有进入Java 7和8,并被无限期搁置。
实验
下面是我testing过的简单实验 – 使1000个ArrayList
的元素为"Hello"
和"World!"
通过add
方法添加到他们,使用两种方法:
方法1:双Brace初始化
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!");
我创build了一个简单的程序来写出一个Java源文件,使用以下两种方法执行1000个初始化:
testing1:
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); } }
testing2:
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
和1000匿名内部类扩展ArrayList
所用的时间,因此计时器没有很高的分辨率。 在我的Windows系统上,分辨率约为15-16毫秒。
两次testing的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
双括号初始化testing程序生成了1000个.class
文件。
这种方法迄今尚未被指出的一个属性是,因为你创build了内部类,整个包含的类被捕获在它的范围内。 这意味着只要你的Set是活着的,它将保留一个指向包含实例( this$0
)的指针,并保持垃圾收集,这可能是一个问题。
这一点,即使一个普通的HashSet工作得很好(甚至更好),一个新的类创build的事实,使我不想使用这个构造(即使我真的渴望语法糖)。
第二个问题:新的HashSet必须是在实例初始化程序中使用的“this”…任何人都可以阐明机制? 我会天真地期待“this”引用初始化“flavor”的对象。
这就是内部类的工作方式。 他们得到自己的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 } }; } }
要清楚创build的匿名子类,您也可以在其中定义方法。 例如重写HashSet.add()
public Set<String> makeSet() { return new HashSet<String>() { { add("hello"); // not HashSet anymore ... } @Override boolean add(String s){ } }; }
参加以下testing课程:
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"); } }; } }
这对我来说看起来并不是非常低效。 如果我担心像这样的performance,我会介绍它。 你的问题#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实例初始化程序),尽pipe我从未在生产代码中看到过这一点。 这是众所周知的。
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文档中的详细信息。
容易泄漏
我已经决定了。性能影响包括:磁盘操作+ unzip(jar),类validation,perm-gen空间(用于Sun的Hotspot JVM)。 然而,最糟糕的是:它容易泄漏。 你不能简单地返回。
Set<String> getFlavors(){ return Collections.unmodifiableSet(flavors) }
因此,如果集合转义到其他类加载器加载的任何其他部分,并且引用保留在其中,则类+ classloader的整个树将被泄漏。 为了避免这种情况,需要拷贝到HashMap中, new LinkedHashSet(new ArrayList(){{add("xxx);add("yyy");}})
不再那么可爱了,我不使用成语,我自己,而是它像new LinkedHashSet(Arrays.asList("xxx","YYY"));
每次有人使用双括号初始化,一只小猫被杀死。
除了语法相当不寻常,并不是真正的习惯(当然,味道是有争议的),你不必要地在你的应用程序中创build了两个重要的问题, 我刚才在这里更详细地提到了这个问题 。
你正在创造太多的匿名类
每次使用双括号初始化时,都会创build一个新类。 比如这个例子:
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次……所有这些只是为了一点“语法糖”而堆起来的内存?
2.您可能会造成内存泄漏!
如果采取上述代码并从方法返回该映射,则该方法的调用者可能会毫无防备地持有非常繁重的资源,而这些资源无法被垃圾回收。 考虑下面的例子:
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有地图文字
为了回答你的实际问题,人们一直在使用这种语法来假装Java有像地图文字一样的东西,类似于现有的数组文字:
String[] array = { "John", "Doe" }; Map map = new HashMap() {{ put("John", "Doe"); }};
有些人可能会发现这种句法刺激。
加载很多类可以在开始时增加几毫秒。 如果启动不那么重要,并且在启动后查看类的效率,则没有区别。
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
要创build集合,您可以使用可变参数工厂方法而不是双括号初始化:
public static Set<T> setOf(T ... elements) { return new HashSet<T>(Arrays.asList(elements)); }
Google Collections库有许多这样的便利方法,以及大量其他有用的function。
至于成语的晦涩问题,我一直都在遇到它,并在生产代码中使用它。 我会更关心程序员,他们被这个习语所困惑,被允许编写产品代码。
除了效率以外,我很less发现自己希望在unit testing之外创build声明式集合。 我相信,双大括号的语法是非常可读的。
另一种实现列表声明式构造的方法是使用Arrays.asList(T ...)
如下所示:
List<String> aList = Arrays.asList("vanilla", "strawberry", "chocolate");
这种方法的局限性当然是你无法控制要生成的特定types的列表。
通常没有什么特别的低效率。 对于JVM而言,通常情况下,您已经创build了一个子类并为其添加了一个构造函数,这在面向对象的语言中是一个正常的日常事情。 我可以想到一些非常虚构的情况,例如你可能会因为这个小类而导致效率低下(例如,由于这个小类你最终会混合使用不同的类别,而传入的类别是完全可以预测的 – – 在后一种情况下,JIT编译器可以进行优化,这在第一个是不可行的)。 但是,真的,我认为重要的情况是非常人为的。
我会从更多的angular度来看待这个问题,你是否想用很多匿名类来“混乱”。 作为一个粗略的指导,考虑使用这个习惯用法,不要使用比如说用于事件处理程序的匿名类。
在(2)中,你在一个对象的构造函数中,所以“this”指的是你正在构造的对象。 这对任何其他构造函数都没有什么不同。
至于(3),这真的取决于谁维护你的代码,我猜。 如果你事先不知道这个,那么我build议使用的一个基准是“你在JDK的源代码中看到了这个吗?” (在这种情况下,我不记得看到许多匿名的初始化者,当然不是这个匿名类的唯一内容)。 在大多数中等规模的项目中,我认为您确实需要程序员在某个时候或某个时候了解JDK源代码,所以在这里使用的任何语法或习惯用语都是“公平的游戏”。 除此之外,我想说,如果你能控制谁在维护代码,那么就用这种语法来训练人们,否则就会发表评论或回避。
我正在研究这个,并决定做一个比有效答案提供的更深入的testing。
这里是代码: https : //gist.github.com/4368924
这是我的结论
我惊讶地发现,在大多数运行testing中,内部启动实际上更快(在某些情况下几乎是双倍的)。 当大量工作时,好处似乎消失了。
有趣的是,在循环中创build3个对象的情况失去了它的好处,而不是其他情况。 我不知道为什么会发生这种情况,应该做更多的testing来得出结论。 创build具体的实现可能有助于避免重载类定义(如果发生了这种情况)
然而,很显然,在大多数情况下,即使是大量的单件build设,也没有太多的开销。
其中一个原因是,每个双重大括号启动会创build一个新的类文件,将整个磁盘块添加到我们的应用程序的大小(或压缩时大约为1k)。 占地面积小,但如果它在很多地方使用,可能会产生影响。 使用这1000次,你可能会添加一个完整的MiB给你的应用程序,这可能涉及embedded式环境。
我的结论是? 只要不被滥用,就可以使用。
让我知道你的想法 :)
我第二Nat的答案,除了我会使用循环,而不是创build,并立即折腾从asList(元素)的隐式列表:
static public Set<T> setOf(T ... elements) { Set set=new HashSet<T>(elements.size()); for(T elm: elements) { set.add(elm); } return set; }
虽然这个语法可以很方便,但是它也增加了很多这样的$ 0引用,因为这些引用是嵌套的,除非在每个引用上设置断点,否则很难将其debugging到初始化程序中。 出于这个原因,我只build议使用这个平庸的setter,尤其是设置为常量,和匿名子类无关紧要的地方(如不涉及序列化)。
Mario Gleichman 介绍了如何使用Java 1.5generics函数来模拟Scala List字面值,但不幸的是你得到了不可变列表。
他定义了这个类:
package literal; public class collection { public static <T> List<T> List(T...elems){ return Arrays.asList( elems ); } }
并如此使用它:
import static literal.collection.List; import static system.io.*; public class CollectionDemo { public void demoList(){ List<String> slist = List( "a", "b", "c" ); List<Integer> iList = List( 1, 2, 3 ); for( String elem : List( "a", "java", "list" ) ) System.out.println( elem ); } }
谷歌collections,现在是番石榴的一部分支持列表build设类似的想法。 Jared Levy在接受采访时说:
在我写的几乎每个Java类中都会出现的使用率最高的function是静态方法,可以减lessJava代码中的重复键击次数。 能够input如下的命令非常方便:
Map<OneClassWithALongName, AnotherClassWithALongName> = Maps.newHashMap();
List<String> animals = Lists.immutableList("cat", "dog", "horse");
2014年7月10日:只要它可以像Python一样简单:
animals = ['cat', 'dog', 'horse']
1) This will call add() for each member. If you can find a more efficient way to put items into a hash set, then use that. Note that the inner class will likely generate garbage, if you're sensitive about that.
2) It seems to me as if the context is the object returned by "new," which is the HashSet.
3) If you need to ask… More likely: will the people who come after you know this or not? Is it easy to understand and explain? If you can answer "yes" to both, feel free to use it.
Double-brace initialization is an unnecessary hack that can introduce memory leaks and other issues
There's no legitimate reason to use this "trick". Guava provides nice immutable collections that include both static factories and builders, allowing you to populate your collection where it's declared in a clean, readable, and safe syntax.
The example in the question becomes:
Set<String> flavors = ImmutableSet.of( "vanilla", "strawberry", "chocolate", "butter pecan");
Not only is this shorter and easier to read, but it avoids the numerous issues with the double-braced pattern described in other answers . Sure, it performs similarly to a directly-constructed HashMap
, but it's dangerous and error-prone, and there are better options.
Any time you find yourself considering double-braced initialization you should re-examine your APIs or introduce new ones to properly address the issue, rather than take advantage of syntactic tricks.