Efficiency of Java "Double Brace Initialization"?
In Hidden Features of Java the top answer mentions Double Brace Initialization, with a very enticing syntax:
Set<String> flavors = new HashSet<String>() {{
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}};
This idiom creates an anonymous inner class with just an instance initializer in it, which "can use any [...] methods in the containing scope".
Main question: Is this as inefficient as it sounds? Should its use be limited to one-off initializations? (And of course showing off!)
Second question: The new HashSet must be the "this" used in the instance initializer ... can anyone shed light on the mechanism?
Third question: Is this idiom too obscure to use in production code?
Summary: Very, very nice answers, thanks everyone. On question (3), people felt the syntax should be clear (though I'd recommend an occasional comment, especially if your code will pass on to developers who may not be familiar with it).
On question (1), the generated code should run quickly. The extra .class files do cause jar file clutter, and slow program startup slightly (thanks to @coobird for measuring that). @Thilo pointed out that garbage collection can be affected, and the memory cost for the extra loaded classes may be a factor in some cases.
Question (2) turned out to be most interesting to me. If I understand the answers, what's happening in DBI is that the anonymous inner class extends the class of the object being constructed by the new operator, and hence has a "this" value referencing the instance being constructed. Very neat.
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 has a good summary of double brace initialization and considers it an anti-pattern.
12/2017: @Basil Bourque notes that in the new Java 9 you can say:
Set<String> flavors = Set.of("vanilla", "strawberry", "chocolate", "butter pecan");
That's for sure the way to go. If you're stuck with an earlier version, take a look at Google Collections' ImmutableSet.
Here's the problem when I get too carried away with anonymous inner classes:
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
These are all classes which were generated when I was making a simple application, and used copious amounts of anonymous inner classes -- each class will be compiled into a separate class
file.
The "double brace initialization", as already mentioned, is an anonymous inner class with a instance initialization block, which means that a new class is created for each "initialization", all for the purpose of usually making a single object.
Considering that the Java Virtual Machine will need to read all those classes when using them, that can lead to some time in the bytecode verfication process and such. Not to mention the increase in the needed disk space in order to store all those class
files.
It seems as if there is a bit of overhead when utilizing double-brace initialization, so it's probably not such a good idea to go too overboard with it. But as Eddie has noted in the comments, it's not possible to be absolutely sure of the impact.
Just for reference, double brace initialization is the following:
List<String> list = new ArrayList<String>() {{
add("Hello");
add("World!");
}};
It looks like a "hidden" feature of Java, but it is just a rewrite of:
List<String> list = new ArrayList<String>() {
// Instance initialization block
{
add("Hello");
add("World!");
}
};
So it's basically a instance initialization block that is part of an anonymous inner class.
Joshua Bloch's Collection Literals proposal for Project Coin was along the lines of:
List<Integer> intList = [1, 2, 3, 4];
Set<String> strSet = {"Apple", "Banana", "Cactus"};
Map<String, Integer> truthMap = { "answer" : 42 };
Sadly, it didn't make its way into neither Java 7 nor 8 and was shelved indefinitely.
Experiment
Here's the simple experiment I've tested -- make 1000 ArrayList
s with the elements "Hello"
and "World!"
added to them via the add
method, using the two methods:
Method 1: Double Brace Initialization
List<String> l = new ArrayList<String>() {{
add("Hello");
add("World!");
}};
Method 2: Instantiate an ArrayList
and add
List<String> l = new ArrayList<String>();
l.add("Hello");
l.add("World!");
I created a simple program to write out a Java source file to perform 1000 initializations using the two methods:
Test 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);
}
}
Test 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);
}
}
Please note, that the elapsed time to initialize the 1000 ArrayList
s and the 1000 anonymous inner classes extending ArrayList
is checked using the System.currentTimeMillis
, so the timer does not have a very high resolution. On my Windows system, the resolution is around 15-16 milliseconds.
The results for 10 runs of the two tests were the following:
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
As can be seen, the double brace initialization has a noticeable execution time of around 190 ms.
Meanwhile, the ArrayList
initialization execution time came out to be 0 ms. Of course, the timer resolution should be taken into account, but it is likely to be under 15 ms.
So, there seems to be a noticeable difference in the execution time of the two methods. It does appear that there is indeed some overhead in the two initialization methods.
And yes, there were 1000 .class
files generated by compiling the Test1
double brace initialization test program.
One property of this approach that has not been pointed out so far is that because you create inner classes, the whole containing class is captured in its scope. This means that as long as your Set is alive, it will retain a pointer to the containing instance ( this$0
) and keep that from being garbage-collected, which could be an issue.
This, and the fact that a new class gets created in the first place even though a regular HashSet would work just fine (or even better), makes me not want to use this construct (even though I really long for the syntactic sugar).
Second question: The new HashSet must be the "this" used in the instance initializer ... can anyone shed light on the mechanism? I'd have naively expected "this" to refer to the object initializing "flavors".
This is just how inner classes work. They get their own this
, but they also have pointers to the parent instance, so that you can call methods on the containing object as well. In case of a naming conflict, the inner class (in your case HashSet) takes precedence, but you can prefix "this" with a classname to get the outer method as well.
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
}
};
}
}
To be clear on the anonymous subclass being created, you could define methods in there as well. For example override HashSet.add()
public Set<String> makeSet() {
return new HashSet<String>() {
{
add("hello"); // not HashSet anymore ...
}
@Override
boolean add(String s){
}
};
}
Taking the following test class:
public class Test {
public void test() {
Set<String> flavors = new HashSet<String>() {{
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}};
}
}
and then decompiling the class file, I see:
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");
}
};
}
}
This doesn't look terribly inefficient to me. If I were worried about performance for something like this, I'd profile it. And your question #2 is answered by the above code: You're inside an implicit constructor (and instance initializer) for your inner class, so " this
" refers to this inner class.
Yes, this syntax is obscure, but a comment can clarify obscure syntax usage. To clarify the syntax, most people are familiar with a static initializer block (JLS 8.7 Static Initializers):
public class Sample1 {
private static final String someVar;
static {
String temp = null;
..... // block of code setting temp
someVar = temp;
}
}
You can also use a similar syntax (without the word " static
") for constructor usage (JLS 8.6 Instance Initializers), although I have never seen this used in production code. This is much less commonly known.
public class Sample2 {
private final String someVar;
// This is an instance initializer
{
String temp = null;
..... // block of code setting temp
someVar = temp;
}
}
If you don't have a default constructor, then the block of code between {
and }
is turned into a constructor by the compiler. With this in mind, unravel the double brace code:
public void test() {
Set<String> flavors = new HashSet<String>() {
{
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}
};
}
The block of code between the inner-most braces is turned into a constructor by the compiler. The outer-most braces delimit the anonymous inner class. To take this the final step of making everything non-anonymous:
public void test() {
Set<String> flavors = new MyHashSet();
}
class MyHashSet extends HashSet<String>() {
public MyHashSet() {
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}
}
For initialization purposes, I'd say there is no overhead whatsoever (or so small that it can be neglected). However, every use of flavors
will go not against HashSet
but instead against MyHashSet
. There is probably a small (and quite possibly negligible) overhead to this. But again, before I worried about it, I would profile it.
Again, to your question #2, the above code is the logical and explicit equivalent of double brace initialization, and it makes it obvious where " this
" refers: To the inner class that extends HashSet
.
If you have questions about the details of instance initializers, check out the details in the JLS documentation.
链接地址: http://www.djcxy.com/p/2332.html上一篇: 用Java获取当前堆栈跟踪
下一篇: Java“双Brace初始化”的效率?