The (double) {{cool}} initialization in Java...
If you've been in the industry long enough, you might have encountered some "cool" engineers who use a little trick called Double-Brace initialization in their Java code – or you might be one of them.
Double-Brace What?
In case you don't know, Double-Brace initialization is a technique that initializes objects inline by doing some *black magek* to make our Java code look cool.
The most popular use case for Double-Brace initialization is with collections:
final var array = new ArrayList<String>(){{ add("Anas"); add("cat"); add("Rust"); }};
But it isn't limited to collections, you can use Double-Brace initialization _virtually_ with any class
public class CustomDoubleBraceInitialization { private static class Thing { String name; int id; void setName(final String n) { this.name = n; } @Override public String toString() { return "The thing name: " + name + ", ID: " + id; } } public static void main(String[] args) { final var thing = new Thing() {{ setName("Anas"); id = 1; }}; System.out.println(thing); } }
Cool, right?
But what's the catch?
As you know, as a software engineers, we can't just live like that and like: Okay cool trick and continue our life, so... Let's dive deeper
First, let's examine how the normal (boring) way works:
import java.util.ArrayList; public class NormalInitialization { public static void main(String[] args) { final var array = new ArrayList<String>(); array.add("Anas"); array.add("cat"); array.add("Rust"); System.out.println(array); } }
Yew, boring; but let's not let the outside fool us, we all know that the real beauty is from the inside right? Right?
when we compile this, javac will give us _one_
.class
file, and when we use javap
on that file
we get something like:
... { public NormalInitialization(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/util/ArrayList."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String Anas 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: aload_1 16: ldc #16 // String cat 18: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 21: pop 22: aload_1 23: ldc #18 // String Rust 25: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 28: pop 29: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream; 32: aload_1 33: invokevirtual #26 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 36: return LineNumberTable: line 5: 0 line 6: 8 line 7: 15 line 8: 22 line 10: 29 line 11: 36 } SourceFile: "NormalInitialization.java"
You might be wondering, "WTF is this?" Well, this is our
glorious bytecode
in its textual representation
– the actual code that gets executed in the JVM. Let's
examine it more closely.
First, let's focus on the relevant part – the code inside
our main
method:
... Code: stack=2, locals=2, args_size=1 0: new #7 // class java/util/ArrayList 3: dup 4: invokespecial #9 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String Anas 11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 14: pop 15: aload_1 16: ldc #16 // String cat 18: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 21: pop 22: aload_1 23: ldc #18 // String Rust 25: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z 28: pop ...
Much cleaner. Now let's examine these instructions one by one.
push
and pop
operations and the locals array to store our variables.
Let's break down the bytecode instructions:
-
0: new #7
- Creates new _uninitialized_ ArrayList object on top of the stack (push) -
3: dup
- Duplicates the top stack value, which its our ArrayList object that we've just created (push) -
4: invokespecial #9
- Calls the constructor (a.k.a.init
function), thats consumes the top value on the stack to construct our object (pop) -
7: astore_1
- Stores the remaining reference in local variable slot 1 (pop) -
8: aload_1
- Loads the ArrayList reference (from local variable 1) onto the stack. (push) -
9: ldc #10
- Loads the String constant "Anas" (from constant pool entry #10) onto the stack (push) -
11: invokevirtual #12
- Calls ArrayList.add(Object) (constant pool entry #12), it consumes the first two items on the stack(this, and Oblect) and it pushes one (the returned boolean) (pop, pop, push) -
14: pop
- discards the boolean result (since we don’t use it). - And just like that the rest instruction do the same operation to add the rest items
Pretty simple bytecode. Now, let's peel back the layers of our 'clever' double-brace initialization and see if it's truly as elegant as it appears.
import java.util.ArrayList; public class DoubleBraceInitialization { public static void main(String[] args) { final var array = new ArrayList<String>() { { add("Anas"); add("cat"); add("Rust"); } }; System.out.println(array); } }
Awww, so aduorable :3, let's see its bytecode
Classfile /tmp/java-lab/DoubleBraceInitialization.class Last modified Apr 10, 2025; size 542 bytes SHA-256 checksum c6e4084b962996a9c42ffdccc26e129f429ebcb21020e7f1ff70e86c3d018faf Compiled from "DoubleBraceInitialization.java" public class DoubleBraceInitialization minor version: 0 major version: 68 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #22 // DoubleBraceInitialization super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 3 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // DoubleBraceInitialization$1 #8 = Utf8 DoubleBraceInitialization$1 #9 = Methodref #7.#3 // DoubleBraceInitialization$1."<init>":()V #10 = Fieldref #11.#12 // java/lang/System.out:Ljava/io/PrintStream; #11 = Class #13 // java/lang/System #12 = NameAndType #14:#15 // out:Ljava/io/PrintStream; #13 = Utf8 java/lang/System #14 = Utf8 out #15 = Utf8 Ljava/io/PrintStream; #16 = Methodref #17.#18 // java/io/PrintStream.println:(Ljava/lang/Object;)V #17 = Class #19 // java/io/PrintStream #18 = NameAndType #20:#21 // println:(Ljava/lang/Object;)V #19 = Utf8 java/io/PrintStream #20 = Utf8 println #21 = Utf8 (Ljava/lang/Object;)V #22 = Class #23 // DoubleBraceInitialization #23 = Utf8 DoubleBraceInitialization #24 = Utf8 Code #25 = Utf8 LineNumberTable #26 = Utf8 main #27 = Utf8 ([Ljava/lang/String;)V #28 = Utf8 SourceFile #29 = Utf8 DoubleBraceInitialization.java #30 = Utf8 NestMembers #31 = Utf8 InnerClasses { public DoubleBraceInitialization(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #7 // class DoubleBraceInitialization$1 3: dup 4: invokespecial #9 // Method DoubleBraceInitialization$1."<init>":()V 7: astore_1 8: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_1 12: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 15: return LineNumberTable: line 5: 0 line 13: 8 line 14: 15 } SourceFile: "DoubleBraceInitialization.java" NestMembers: DoubleBraceInitialization$1 InnerClasses: #7; // class DoubleBraceInitialization$1
The reason that i bought the full output of javap
command
this time that because their's some magek happening here
So, let's analyze this bytecode more closely. We'll focus on
three key sections: the Constant pool
, the
Code
segment, and the sneaky
InnerClasses
declaration. First, we'll examine
the Cnstant pool
section.
... Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Class #8 // DoubleBraceInitialization$1 #8 = Utf8 DoubleBraceInitialization$1 #9 = Methodref #7.#3 // DoubleBraceInitialization$1."<init>":()V #10 = Fieldref #11.#12 // java/lang/System.out:Ljava/io/PrintStream; #11 = Class #13 // java/lang/System #12 = NameAndType #14:#15 // out:Ljava/io/PrintStream; #13 = Utf8 java/lang/System #14 = Utf8 out #15 = Utf8 Ljava/io/PrintStream; #16 = Methodref #17.#18 // java/io/PrintStream.println:(Ljava/lang/Object;)V #17 = Class #19 // java/io/PrintStream #18 = NameAndType #20:#21 // println:(Ljava/lang/Object;)V #19 = Utf8 java/io/PrintStream #20 = Utf8 println #21 = Utf8 (Ljava/lang/Object;)V #22 = Class #23 // DoubleBraceInitialization #23 = Utf8 DoubleBraceInitialization #24 = Utf8 Code #25 = Utf8 LineNumberTable #26 = Utf8 main #27 = Utf8 ([Ljava/lang/String;)V #28 = Utf8 SourceFile #29 = Utf8 DoubleBraceInitialization.java #30 = Utf8 NestMembers #31 = Utf8 InnerClasses ....
Wait a sec, where is our strings? isn't those supposed to be constants?
Actually, yes, any hard coded value should end in the constant pool, but where is our strings then?
and if we lock closer we'll spot a strange line in our pool, actually *lines*
... #7 = Class #8 // DoubleBraceInitialization$1 #8 = Utf8 DoubleBraceInitialization$1 ... #30 = Utf8 NestMembers #31 = Utf8 InnerClasses ....
I don't remember having an inner class in our code, where that comes from?
Hmm.. let's look at the code section, maybe it explains something
... Code: stack=2, locals=2, args_size=1 0: new #7 // class DoubleBraceInitialization$1 3: dup 4: invokespecial #9 // Method DoubleBraceInitialization$1."<init>":()V 7: astore_1 8: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_1 12: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 15: return ...
Notice something interesting here? Unlike our previous
example there's no explicit add()
method calls
or any mention to our strings
Instead we're creating an instance from a mysterious
DoubleBraceInitialization$1
class:
... 0: new #7 // class DoubleBraceInitialization$1 3: dup 4: invokespecial #9 // Method DoubleBraceInitialization$1."<init>":()V 7: astore_1 ...
This is the anonymous subclass that double-brace initialization creates behind the scenes.
Looking at the InnerClasses
section confirms
this:
InnerClasses: #7; // class DoubleBraceInitialization$1
This shows that our double-brace initialization actually creates a new anonymous inner class that extends ArrayList. The initialization block (the second set of braces) becomes the instance initializer for this anonymous class.
If we examine the generated
DoubleBraceInitialization$1.class
file, we’ll
see the true cost of double-brace initialization:
Classfile /tmp/java-lab/DoubleBraceInitialization$1.class Last modified Apr 10, 2025; size 545 bytes SHA-256 checksum b2aea284a68cf6aa5d97dcbc1c5bb12915db8cb6e8f1bbfe3fc152873aa7cc53 Compiled from "DoubleBraceInitialization.java" class DoubleBraceInitialization$1 extends java.util.ArrayList<java.lang.String> minor version: 0 major version: 68 flags: (0x0020) ACC_SUPER this_class: #10 // DoubleBraceInitialization$1 super_class: #2 // java/util/ArrayList interfaces: 0, fields: 0, methods: 1, attributes: 5 Constant pool: #1 = Methodref #2.#3 // java/util/ArrayList."<init>":()V #2 = Class #4 // java/util/ArrayList #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/util/ArrayList #5 = Utf8 <init> #6 = Utf8 ()V #7 = String #8 // Anas #8 = Utf8 Anas #9 = Methodref #10.#11 // DoubleBraceInitialization$1.add:(Ljava/lang/Object;)Z #10 = Class #12 // DoubleBraceInitialization$1 #11 = NameAndType #13:#14 // add:(Ljava/lang/Object;)Z #12 = Utf8 DoubleBraceInitialization$1 #13 = Utf8 add #14 = Utf8 (Ljava/lang/Object;)Z #15 = String #16 // cat #16 = Utf8 cat #17 = String #18 // Rust #18 = Utf8 Rust #19 = Utf8 Code #20 = Utf8 LineNumberTable #21 = Utf8 Signature #22 = Utf8 Ljava/util/ArrayList<Ljava/lang/String;>; #23 = Utf8 SourceFile #24 = Utf8 DoubleBraceInitialization.java #25 = Utf8 EnclosingMethod #26 = Class #27 // DoubleBraceInitialization #27 = Utf8 DoubleBraceInitialization #28 = NameAndType #29:#30 // main:([Ljava/lang/String;)V #29 = Utf8 main #30 = Utf8 ([Ljava/lang/String;)V #31 = Utf8 NestHost #32 = Utf8 InnerClasses { DoubleBraceInitialization$1(); descriptor: ()V flags: (0x0000) Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/util/ArrayList."<init>":()V 4: aload_0 5: ldc #7 // String Anas 7: invokevirtual #9 // Method add:(Ljava/lang/Object;)Z 10: pop 11: aload_0 12: ldc #15 // String cat 14: invokevirtual #9 // Method add:(Ljava/lang/Object;)Z 17: pop 18: aload_0 19: ldc #17 // String Rust 21: invokevirtual #9 // Method add:(Ljava/lang/Object;)Z 24: pop 25: return LineNumberTable: line 5: 0 line 7: 4 line 8: 11 line 9: 18 line 1: 25 } Signature: #22 // Ljava/util/ArrayList<Ljava/lang/String;>; SourceFile: "DoubleBraceInitialization.java" EnclosingMethod: #26.#28 // DoubleBraceInitialization.main NestHost: class DoubleBraceInitialization InnerClasses: #10; // class DoubleBraceInitialization$1
Noticed the class definition?
class DoubleBraceInitialization$1 extends java.util.ArrayList<java.lang.String>
it secretly creates an anonymous inner class that extends
ArrayList
. The second set of braces—the
initialization block—becomes an instance initializer in this
anonymous class, containing all our
add()
calls.
This is why the main class’s bytecode appears simpler: the initialization logic is offloaded to the hidden class.
Isn't that a good thing?
No.
By using double-brace initializtion you'r adding complixty, increasing memory usage, leaking memory, making serialization/deserialization harder, killing kittens, and frocing Java to be someone she's clearly isn't.
Each time you use double brace initialisation a new class is made
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"); }}); }}); }};
... will produce these classes:
Test$1$1$1.class Test$1$1$2.class Test$1$1.class Test$1.class Test.class
Just immagen being the poor JVM, who has to load all these classes and execute them and keeping track of them.
The complete story
So, with some compiler hacking we can see what's the final code after the annotation
stage or desuggring if we can say
public class DoubleBraceInitialization { public DoubleBraceInitialization() { super(); } public static void main(String[] args) { final DoubleBraceInitialization$1 array = new ArrayList<String>(){ () { super(); } { add("Anas"); add("cat"); add("Rust"); } }; System.out.println(array); } }
and that's an prove that what matters most is the inside, not the outside
"Every time someone uses double brace initialisation, a kitten gets killed." – Anonymous Stack Overflow comment
Bye..█