Double brace initialization creates a collection and fills it in one expression by declaring an anonymous subclass of the collection (the outer braces) that contains an instance initializer block (the inner braces) with the add() or put() calls. It is not a separate language feature, only a combination of two features that exist since Java 1.1.
Developers used it before Java 9 to build small constant collections and test data in one statement. Today we see it mostly in older code, because the factory methods List.of(), Set.of() and Map.of() do the same job without the hidden costs.
The following example builds the same set of fruit names twice, once with double braces and once with Set.of().
Set<String> fruits = new HashSet<>() {{
add("apple");
add("banana");
}};
boolean hasApple = fruits.contains("apple"); // true
boolean isAnonymous = fruits.getClass().isAnonymousClass(); // true
boolean isPlainHashSet = fruits.getClass() == HashSet.class; // false
Set<String> modern = Set.of("apple", "banana");
boolean sameContent = fruits.equals(modern); // true
Notice that the object is not a HashSet but an instance of a new anonymous class that extends HashSet. We look at what the compiler generates, the four problems that follow from it, the alternatives for every Java version and how to replace the idiom in existing code.
1. How Double Brace Initialization Works
The first pair of braces after new HashSet<>() starts the body of an anonymous class, so the compiler creates a subclass of HashSet with a generated name such as PantryService$1. The second pair is an instance initializer, a block that runs every time an instance of that class is created, right after the HashSet constructor. Inside the initializer, add(“apple”) is a call on the new object itself, the same as this.add(“apple”).

Written out with a named class, the same code looks like this. The anonymous version only hides the class declaration.
class FruitSet extends HashSet<String> {
{
add("apple");
add("banana");
}
}
Set<String> named = new FruitSet();
boolean sameAsBraces = named.equals(Set.of("apple", "banana")); // true
Every place in the code that uses double braces is a separate class. A test class with 40 such fixtures compiles to 40 extra .class files, and the JVM loads each of them, which costs startup time and metaspace memory but nothing per element.
2. Problems With Double Brace Initialization
Because the result is an instance of an anonymous inner class, it inherits the rules of inner classes. Four of them cause real bugs or surprises, and the first one is the reason IDE inspections such as the IntelliJ IDEA double brace check flag the idiom.
2.1. A Hidden Reference to the Outer Object
An anonymous class created in an instance method keeps a reference to the object whose method created it, so the collection keeps that object alive as long as the collection itself is reachable. The reference is a synthetic field named this$0.
Say a pantry service keeps a large stock history in memory and returns its default reorder limits as a double brace map. A caching layer stores the map in a static field for the whole life of the app. The service instance can never be garbage collected, together with its history, even after the app has replaced it with a new one.
class PantryService {
private final List<String> stockHistory = new ArrayList<>(List.of("apple", "banana"));
Map<String, Integer> reorderLimits() {
return new HashMap<>() {{
put("apple", 5);
put("banana", 3);
}};
}
static Map<String, Integer> reorderLimitsStatic() {
return new HashMap<>() {{
put("apple", 5);
}};
}
}
Map<String, Integer> limits = new PantryService().reorderLimits();
String hiddenField = limits.getClass().getDeclaredFields()[0].getName(); // "this$0"
String fieldType = limits.getClass().getDeclaredFields()[0].getType().getSimpleName(); // "PantryService"
int staticFields = PantryService.reorderLimitsStatic().getClass().getDeclaredFields().length; // 0
The static method version has no outer object, so it has no hidden field. Since Java 18, javac also leaves out the field when an anonymous class never uses the outer object, but it keeps the field in serializable classes. Every JDK collection is serializable, so a double brace collection created in an instance method still holds this$0 on JDK 25.
2.2. Serialization Fails or Drags in the Outer Object
Java serialization writes every non-transient field of an object, including this$0. If the outer class is not Serializable, writing the collection fails with a NotSerializableException that names the outer class. If the outer class is serializable, the whole outer object is written with the collection, which is rarely what we want. The article on Java serialization covers the rules in detail.
The helper returns the name of the exception, and the exception message names the class that is not serializable, here PantryService.
static String serialize(Object value) {
try (ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream())) {
out.writeObject(value);
return "ok";
} catch (IOException e) {
return e.getClass().getSimpleName();
}
}
String braces = serialize(new PantryService().reorderLimits()); // "NotSerializableException"
String plain = serialize(new HashMap<>(Map.of("apple", 5))); // "ok"
2.3. The Runtime Class Is Not the Collection Class
The JDK collections compare contents in equals(), so a double brace HashSet is equal to a plain HashSet or a Set.of() set with the same elements. Problems start in code that checks the exact class, such as getClass() == HashSet.class, an equals() method written with getClass() in our own classes, or a serialization library that writes type information.
2.4. Final Classes and the Diamond Operator
Double braces need a class that can be extended. They do not work with final classes, records, or the unmodifiable types returned by List.of(), and they cannot produce an immutable collection. Before Java 9, the diamond operator was not allowed with anonymous classes, so older code repeats the type arguments, as in new HashSet<String>() {{ … }}. Since Java 9 (JEP 213), new HashSet<>() {{ … }} compiles when the inferred type is denotable.
3. Alternatives to Double Brace Initialization
Every case where we would use double braces has a better option in current Java. The right one depends on whether the collection must be modifiable, whether it may contain null and whether the insertion order matters.
| Approach | Since | Modifiable | Allows null | Keeps order |
|---|---|---|---|---|
| List.of(), Set.of(), Map.of() | Java 9 | No | No, NullPointerException | List yes, Set and Map no |
| Map.ofEntries(Map.entry(…)) | Java 9 | No | No | No |
| new ArrayList<>(List.of(…)) | Java 9 | Yes | No (in the input) | Yes |
| Stream.of(…).collect(Collectors.toCollection(TreeSet::new)) | Java 8 | Yes | Depends on the collection | Depends on the collection |
| Collections.addAll(set, …) | Java 5 | Yes | Yes | Depends on the collection |
| Double brace initialization | Java 1.1 | Yes | Yes | Depends on the collection |
The immutable collection factory methods are the default choice for constants and test data. They reject null elements, Set.of() and Map.of() also reject duplicates, and their iteration order is not specified.
List<String> fruitList = List.of("apple", "banana"); // [apple, banana]
Map<String, Integer> stock = Map.of("apple", 5, "banana", 3);
int apples = stock.get("apple"); // 5
Map<String, Integer> manyItems = Map.ofEntries(
Map.entry("apple", 5),
Map.entry("banana", 3),
Map.entry("cherry", 7));
int itemCount = manyItems.size(); // 3
boolean added = fruitList.add("cherry"); // UnsupportedOperationException
Set<String> duplicates = Set.of("apple", "apple"); // IllegalArgumentException: duplicate element: apple
When the code adds more elements later, we copy the factory result into a regular collection. For a LinkedHashMap or a sorted set, a stream collector or Collections.addAll() creates the exact type we need, and stream collectors can also return unmodifiable results.
List<String> shoppingList = new ArrayList<>(List.of("apple", "banana"));
shoppingList.add("cherry");
List<String> grown = shoppingList; // [apple, banana, cherry]
Set<String> sorted = Stream.of("banana", "apple").collect(Collectors.toCollection(TreeSet::new)); // [apple, banana]
Set<String> ordered = new LinkedHashSet<>();
Collections.addAll(ordered, "banana", "apple");
Set<String> insertionOrder = ordered; // [banana, apple]
Maps with more than ten entries need Map.ofEntries(), because Map.of() has overloads for up to ten key-value pairs only.
4. Replacing Double Braces in Existing Code
Double braces show up in two places in older code bases, namely test fixtures and objects other than collections. Both can be replaced without changing behavior, as long as we check whether the code later modifies the collection.
A typical test fixture from a Java 7 project builds the expected map with double braces. The factory method version is shorter, and an accidental put() in the test fails fast with an UnsupportedOperationException.
// Before: Java 7 style fixture
Map<String, Integer> expectedOld = new HashMap<String, Integer>() {{
put("apple", 5);
put("banana", 3);
}};
// After: Java 9+ style fixture
Map<String, Integer> expectedNew = Map.of("apple", 5, "banana", 3);
boolean sameFixture = expectedOld.equals(expectedNew); // true
For other objects, such as a Swing JFrame configured with setTitle() and setSize() inside double braces, the replacement is a local variable and a few plain method calls, or a builder when the class offers one.
5. Double Brace Initialization FAQs
A reviewer who spots double braces in a pull request wants to know about leaks, speed, thread safety and the shortest replacement.
5.1. Is double brace initialization an anti-pattern?
Yes, in production code. It creates a class per use, can keep the outer object alive and breaks serialization, while List.of(), Set.of() and Map.of() give the same one-line syntax without these costs. In a static context, such as a static field or a test helper, the risks are smaller, but the factory methods are still shorter.
5.2. Does double brace initialization cause memory leaks?
Yes, it can. A double brace collection created in an instance method references the object that created it, as shown in section 2.1. The collection becomes a leak when it lives longer than that object, for example in a static cache, a session or a singleton.
5.3. Is double brace initialization slower than normal initialization?
Only the first use of each occurrence is slower, because the JVM loads one extra class for it. After that, creating the object costs about the same as a normal constructor followed by add() calls. The main costs are memory and design problems, not speed.
5.4. How do we initialize a HashMap in one line?
We use new HashMap<>(Map.of(“apple”, 5, “banana”, 3)) when the map must stay modifiable, and Map.of(…) when it is a constant. For more than ten entries, we use Map.ofEntries() as shown in section 3.
5.5. Is a double brace collection thread-safe?
No. The anonymous subclass has the same thread safety as its parent class, so a double brace HashMap is not thread-safe. The unmodifiable collections from Map.of() are safe to share between threads after they are published, because nobody can change them.
6. Conclusion
Double brace initialization is an anonymous subclass with an instance initializer. It looks like a collection literal, but each use adds a class, the object keeps a reference to the enclosing instance when it is created in an instance method, and serialization either fails or writes the outer object as well.
Since Java 9, the factory methods List.of(), Set.of(), Map.of() and Map.ofEntries() cover constants and test data, and a copy into new ArrayList<>(…) or new HashMap<>(…) covers collections that must change later. More collection types and their factory methods are in the Java Collections guide.
7. References
- JLS 25, Anonymous Class Declarations
- JLS 25, Instance Initializers
- Set Javadoc, Unmodifiable Sets
- JEP 269, Convenience Factory Methods for Collections
- JDK-8271623, Omit enclosing instance fields from inner classes that don’t use it
Happy Learning !!