Question

Java Double Brace Initialization: Efficiency, Risks, and Better Alternatives

javaperformancecollectionsinitialization

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:

  1. new HashSet<String>() { ... } creates an anonymous class that extends HashSet<String>.
  2. 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.
  • In a non-static context, the anonymous inner object can hold an implicit reference to the enclosing object. Keeping the collection alive may then keep the enclosing object alive too.
  • The result has an anonymous runtime type rather than exactly HashSet, which can make debugging, serialization, frameworks, and stack traces less predictable.
  • Many Java developers do not immediately recognize the syntax.

For production code, prefer ordinary initialization, factory methods, or a named helper when mutability and collection type matter.

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, this is 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.”

Take Quiz

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 null elements, and rejects duplicate elements.

Mutable set from fixed initial values

When later code must add or remove values, create a mutable copy:

Set<String> flavors = new HashSet<>(Set.of(
    "vanilla",
    "strawberry",
    "chocolate"
));

flavors.add("butter pecan");

For Java 8 or earlier:

Set<String> flavors = new HashSet<>(Arrays.asList(
    "vanilla",
    "strawberry",
    "chocolate"
));

The HashSet is mutable even though Arrays.asList(...) itself returns a fixed-size list.

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:

  1. Java sees new HashSet<String>() { ... } and uses an anonymous subclass of HashSet<String>.
  2. Java allocates one instance of that anonymous subclass.
  3. The HashSet constructor runs first.
  4. Java runs the anonymous class's instance initializer block.
  5. Within the block, this is the new anonymous HashSet instance.
  6. add(...) therefore calls this.add(...) on that set.
  7. The initializer reads restaurantName from the enclosing Menu object.
  8. Java returns the initialized set.

The ability to read restaurantName is also the source of a potential retention problem: the anonymous object may keep a reference to the particular Menu instance that created it. A static context has no enclosing instance to capture.

A roughly equivalent expanded form is:

class Menu {
    private final String restaurantName = "Sweet Shop";

    private class FlavorSet extends HashSet<String> {
        FlavorSet() {
            add(restaurantName + ": vanilla");
            add(restaurantName + ": chocolate");
        }
    }

    Set<String> createFlavors() {
        return new FlavorSet();
    }
}

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.

Take Quiz

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 new IllegalArgumentException("Unsupported sort field: " + sort);
    }
}

A named class is appropriate if construction needs real behavior, validation, or reusable logic. An anonymous subclass used only to preload values usually adds hidden complexity without adding a useful abstraction.

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 Set.of when mutation is required

This compiles but fails at runtime:

Set<String> flavors = Set.of("vanilla", "chocolate");
flavors.add("strawberry"); // throws UnsupportedOperationException

Use a mutable copy when changes are intended:

Set<String> flavors = new HashSet<>(Set.of("vanilla", "chocolate"));
flavors.add("strawberry");

Expecting Arrays.asList to create a mutable list

List<String> flavors = Arrays.asList("vanilla", "chocolate");
flavors.add("strawberry"); // throws UnsupportedOperationException

For a resizable list:

List<String> flavors = new ArrayList<>(Arrays.asList("vanilla", "chocolate"));

Forgetting set rules

A set does not store duplicates. Also, Set.of("vanilla", "vanilla") throws IllegalArgumentException, while HashSet silently keeps one copy.

Comparisons

ApproachJava versionMutable?Main characteristics
Double brace initializationAnyUsually yesCreates an anonymous subclass and runs an initializer block; concise but generally discouraged in production.
Set.of("a", "b")9+NoClear immutable set; rejects null and duplicate values.
new HashSet<>(Set.of(...))9+YesClear way to start with values and allow later changes.
new HashSet<>(Arrays.asList(...))8 and earlierYesCommon compatibility option; the resulting HashSet is mutable.
Arrays.asList(...)AnyFixed sizeProduces a list, not a set; elements can be replaced but the size cannot change.

DBI is closest in outcome to creating a mutable HashSet and calling add repeatedly. Its key difference is that it creates an extra anonymous class to make those calls happen during construction.

Take Quiz

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.
  • this inside 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 null and 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 Set.of for fixed values; use new HashSet<>(...) 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 clear throws UnsupportedOperationException. Wrap it in new HashSet<>(...) if a mutable set is needed.

What should I use on Java 8 instead of Set.of?

A common option is new HashSet<>(Arrays.asList("a", "b")). The final HashSet is mutable.

Is double brace initialization acceptable in tests?

It can be acceptable in small, local tests, but Set.of, List.of, or ordinary collection setup is usually just as readable and avoids the hidden behavior.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more