Question
Java Double Brace Initialization: Efficiency, Risks, and Better Alternatives
Question
Consider this Java collection initialization pattern, often called double brace initialization:
Set<String> flavors = new HashSet<String>() {{
add("vanilla");
add("strawberry");
add("chocolate");
add("butter pecan");
}};
How efficient is this approach? Should it be limited to one-off initialization or demonstration code?
How does the instance initializer know that this refers to the newly created HashSet?
Finally, is this syntax too obscure or risky to use in production Java code, and what are clearer alternatives?
Short Answer
Double brace initialization combines an anonymous subclass with an instance initializer block. It is valid Java and usually fast for a single execution, but it creates an extra class and can accidentally retain an enclosing object. In modern Java, collection factory methods such as Set.of(...) are usually clearer and safer.
Concept
Double brace initialization (DBI) is named after its two adjacent brace pairs:
new HashSet<String>() {{
add("vanilla");
}}
It is not special collection syntax. Java interprets it as two language features used together:
new HashSet<String>() { ... }creates an anonymous class that extendsHashSet<String>.- The inner
{ ... }is an instance initializer block. Java runs it whenever an instance of that anonymous class is constructed.
A more explicit conceptual version looks like this:
class FlavorSet extends HashSet<String> {
{
add("vanilla");
add("strawberry");
}
}
Set<String> flavors = new FlavorSet();
The compiler generates a synthetic anonymous class, often with a name similar to MyClass$1. The initializer runs as part of constructing that object.
Why it can be a poor default
The collection operations themselves are not inherently slow. The important costs and risks are structural:
- Each DBI expression can create a distinct anonymous class.
- More classes mean extra class metadata and class-loading work, especially if this style is used widely.
Mental Model
Think of new HashSet<String>() { ... } as ordering a custom version of a HashSet from a workshop.
- The first braces say: “Build a tiny unnamed subclass of
HashSet.” - The second braces are the setup instructions run whenever that custom object is built.
- Inside those setup instructions,
thisis the object currently being assembled: the anonymous subclass instance.
So this code:
new HashSet<String>() {{
add("vanilla");
}}
means: “Create a custom HashSet object, then immediately add vanilla to that same object while it is being constructed.”
Syntax and Examples
The DBI form is:
new ParentType(arguments) {
{
// instance initializer
}
};
For a set:
Set<String> flavors = new HashSet<String>() {{
add("vanilla");
add("strawberry");
add("chocolate");
}};
The add calls work because the anonymous class inherits HashSet methods. They are effectively method calls on this:
add("vanilla");
// equivalent in meaning to:
this.add("vanilla");
Preferred modern Java: immutable collections
If the values are fixed, Java 9 and later provides collection factory methods:
Set<String> flavors = Set.of(
"vanilla",
"strawberry",
"chocolate",
"butter pecan"
);
Set.of(...) is concise and communicates that the set should not change. It returns an unmodifiable set, rejects elements, and rejects duplicate elements.
Step by Step Execution
Consider this code inside an instance method:
class Menu {
private final String restaurantName = "Sweet Shop";
Set<String> createFlavors() {
return new HashSet<String>() {{
add(restaurantName + ": vanilla");
add(restaurantName + ": chocolate");
}};
}
}
Execution proceeds like this:
- Java sees
new HashSet<String>() { ... }and uses an anonymous subclass ofHashSet<String>. - Java allocates one instance of that anonymous subclass.
- The
HashSetconstructor runs first. - Java runs the anonymous class's instance initializer block.
- Within the block,
thisis the new anonymousHashSetinstance. add(...)therefore callsthis.add(...)on that set.- The initializer reads
restaurantNamefrom the enclosingMenuobject.
Real World Use Cases
DBI can be convenient in short-lived test code where its trade-offs are understood:
@Test
void acceptsKnownFlavor() {
Set<String> supported = new HashSet<String>() {{
add("vanilla");
add("chocolate");
}};
assertTrue(supported.contains("vanilla"));
}
However, even tests are often clearer with Set.of(...):
Set<String> supported = Set.of("vanilla", "chocolate");
In application code, initial collections are common in areas such as:
- Configuration: allowed file extensions, user roles, or feature names.
- Validation: accepted request statuses or required fields.
- API responses: fixed sets of capabilities or supported formats.
- Data processing: lookup tables and category sets.
For these uses, choose an immutable collection when the values are constants. Use a normal mutable collection only when the application truly changes it.
Real Codebase Usage
Production Java code normally makes mutability explicit.
Constants: use immutable factories
private static final Set<String> SUPPORTED_FORMATS = Set.of(
"json", "csv", "xml"
);
This is clear, has no anonymous subclass, and prevents accidental modification.
Per-request mutable state: use a normal constructor
Set<String> requestedFields = new HashSet<>();
for (String field : request.getFields()) {
requestedFields.add(field);
}
Build mutable collections from a known source
Set<String> permissions = new HashSet<>(defaultPermissions);
permissions.add("EXPORT_REPORTS");
Validate early with a set
private static final Set<String> ALLOWED_SORTS = Set.of("name", "createdAt");
void validateSort(String sort) {
if (!ALLOWED_SORTS.contains(sort)) {
throw ( + sort);
}
}
Common Mistakes
Assuming DBI creates an ordinary HashSet
It creates an anonymous subclass of HashSet, not a plain HashSet instance. Usually this is harmless, but code that relies on exact runtime classes, serialization behavior, or framework conventions can be surprised.
Accidentally retaining an enclosing object
In an instance context, this can capture the outer instance:
class LargeScreen {
private final byte[] largeData = new byte[10_000_000];
Set<String> buildOptions() {
return new HashSet<String>() {{
add("compact");
}};
}
}
If the returned set lives for a long time, its anonymous class instance may also keep the LargeScreen object reachable. Prefer this instead:
Set<String> buildOptions() {
return new HashSet<>(Set.of("compact"));
}
Using when mutation is required
Comparisons
| Approach | Java version | Mutable? | Main characteristics |
|---|---|---|---|
| Double brace initialization | Any | Usually yes | Creates an anonymous subclass and runs an initializer block; concise but generally discouraged in production. |
Set.of("a", "b") | 9+ | No | Clear immutable set; rejects null and duplicate values. |
new HashSet<>(Set.of(...)) | 9+ | Yes | Clear way to start with values and allow later changes. |
new HashSet<>(Arrays.asList(...)) | 8 and earlier | Yes | Common compatibility option; the resulting HashSet is mutable. |
Cheat Sheet
// Double brace initialization: valid, but usually avoid in production
Set<String> values = new HashSet<String>() {{
add("a");
add("b");
}};
- Outer braces: anonymous subclass body.
- Inner braces: instance initializer block.
thisinside the initializer is the newly created anonymous subclass instance.- In a non-static context, the object can retain a reference to its enclosing instance.
- Repeated DBI expressions can create additional anonymous classes.
// Java 9+: immutable set
Set<String> values = Set.of("a", "b");
- Cannot add or remove elements.
- Rejects
nulland duplicates.
// Java 9+: mutable set with initial values
Set<String> values = new HashSet<>(Set.of("a", "b"));
// Java 8 and earlier: mutable set with initial values
Set<String> values = new HashSet<>(Arrays.asList("a", "b"));
Rule of thumb: use for fixed values; use when later mutation is required; avoid DBI unless its trade-offs are intentional.
FAQ
Is double brace initialization slow in Java?
The add operations themselves are ordinary method calls and are typically fast. The concern is the extra anonymous class, its metadata, class loading, and possible captured outer reference—not usually a large cost for one object, but unnecessary overhead as a common pattern.
Why are there two pairs of braces in double brace initialization?
The first pair is the body of an anonymous class extending the constructed type. The second pair is an instance initializer block inside that anonymous class.
What does this refer to in a double brace initializer?
It refers to the newly constructed anonymous subclass instance. Because that instance extends HashSet, it can call inherited methods such as add.
Does double brace initialization cause a memory leak?
Not automatically. But when used from a non-static context, the anonymous object may retain its enclosing instance. If the collection outlives that enclosing object, this can cause unintended memory retention.
Is Set.of a replacement for double brace initialization?
For fixed sets in Java 9+, yes, in most cases. Set.of is clearer and avoids an anonymous class. Remember that its result is unmodifiable.
Can I add elements to a set created with Set.of?
No. Calling add, remove, or throws . Wrap it in if a mutable set is needed.
Mini Project
Description
Create a small flavor catalog that separates a fixed set of supported flavors from a customer’s mutable selection. This models a common application design: immutable configuration plus per-user state that may change.
Goal
Use modern collection initialization to validate flavors and safely update a customer order without double brace initialization.
Requirements
Use Set.of(...) to define the supported flavors.
Create a mutable HashSet for a customer order.
Add flavors only when they are supported.
Reject an unsupported flavor with a clear message.
Print the final customer order.
Keep learning
Related questions
Add External JAR Files to an IntelliJ IDEA Java Project
Learn how to add external JAR dependencies to an IntelliJ IDEA Java project using module libraries, and when to use Maven or Gradle instead.
Avoiding Java Code in JSP with JSP 2: EL and JSTL Explained
Learn how to avoid Java scriptlets in JSP 2 using Expression Language and JSTL, with examples, best practices, and common mistakes.
Call a Method After a Delay in Android Java
Learn how to run Java code after a delay in Android using Handler.postDelayed, manage the main thread, and cancel callbacks safely.