Back to Blog
Java

Creating and Using Java Immutable Sets

java immutable set: Learn how to create and use immutable sets in Java with Set.of, understand their behavior, and avoid common pitfalls with unmodifiable views.

immutable collectionsSet.ofJava collectionsunmodifiable setJava 9
A locked set icon with Java code elements, representing an immutable set in Java

When you need a set that cannot change after creation, Java offers several options. The most direct approach is Set.of, introduced in Java 9, which returns a truly immutable set. But the term "immutable" is often confused with "unmodifiable," and the difference matters at runtime. This article focuses on creating and using a java immutable set correctly, including the behavior you can rely on and the traps that appear in production code.

Creating an Immutable Set with Set.of

The simplest way to create an immutable set is to use the static factory method Set.of. It accepts zero or more elements and returns a set that cannot be modified.

Set<String> colors = Set.of("red", "green", "blue");

This set is immutable: any attempt to call add, remove, or clear throws UnsupportedOperationException. The factory method is overloaded to accept up to ten elements, and a varargs version handles larger sets. The resulting set is also serializable if the elements are serializable.

Set.of does not guarantee iteration order. The iteration order is unspecified and may change between different JVM runs or even between different calls. If you need a stable order, use LinkedHashSet and then wrap it, or consider Collections.unmodifiableSet with a LinkedHashSet backing. But if order is not a requirement, Set.of is the cleanest choice.

Null Elements Are Rejected

A critical behavior of Set.of is that it does not allow null elements. Attempting to include null throws NullPointerException at creation time.

Set<String> set = Set.of("a", null); // NullPointerException

This is different from Collections.unmodifiableSet, which does not reject null if the backing set allows it. The null restriction is a deliberate design decision to avoid ambiguity in contains and remove operations. If your data may contain null, you cannot use Set.of directly. You would need to filter nulls or use a different approach.

Unmodifiable View vs. Immutable Set

Collections.unmodifiableSet returns a view over an existing set. The view itself cannot be modified, but the backing set can still change. This is not an immutable set.

Set<String> backing = new HashSet<>(); backing.add("a"); Set<String> unmodifiable = Collections.unmodifiableSet(backing); backing.add("b"); // This changes the unmodifiable view too System.out.println(unmodifiable); // [a, b]

In contrast, Set.of creates a new set with no backing reference. There is no way to modify it after creation. This distinction is crucial when you pass a set to another component and want to guarantee that it cannot change. If you use an unmodifiable view, any code that still holds a reference to the backing set can mutate it, breaking the contract.

Iteration Order and Equality

The iteration order of Set.of is intentionally unspecified. This allows the implementation to choose a hash-based or other layout that optimizes memory and performance. In practice, the order may vary between different JVM versions and even between different runs of the same program. Code that relies on iteration order should not use Set.of.

Equality, however, is well-defined. Two sets are equal if they contain the same elements, regardless of order. Set.of respects the equals contract. So you can safely compare immutable sets with equals and use them as map keys, provided the elements themselves have proper equals and hashCode implementations.

Performance and Memory Considerations

Set.of is designed for small sets, and the implementation is highly optimized for memory. The JVM may use a specialized representation that stores elements in a compact array-like structure, avoiding the overhead of a full HashMap. For sets with a small, fixed number of elements, this can reduce memory usage significantly compared to a HashSet.

There is no performance benefit for large sets; the varargs overload may allocate an array, but the internal representation is still efficient. The main cost is that Set.of is immutable, so any modification requires creating a new set. If you need to modify the set frequently, an immutable set is not the right choice. Use a mutable HashSet or LinkedHashSet instead.

When to Use an Immutable Set

Use an immutable set when you have a fixed collection of values that should never change. Common examples include configuration constants, allowed enum values, or a set of default permissions. Passing an immutable set to a method guarantees that the method cannot accidentally alter your data. It also makes your code easier to reason about because the set's state is constant.

Avoid immutable sets when the set is large and changes often. Creating a new set for every modification adds overhead and can lead to memory churn. Also, if you need to support null elements, Set.of is not an option. In that case, consider using a mutable set and carefully controlling access, or use a third-party library like Guava's ImmutableSet, which also rejects null but offers different features.

Common Pitfalls and How to Avoid Them

One common mistake is assuming that Set.of preserves insertion order. It does not. If order matters, use LinkedHashSet and wrap it with Collections.unmodifiableSet to get an unmodifiable view with predictable iteration order.

Another pitfall is using Collections.unmodifiableSet when you need true immutability. If the backing set is still accessible, the "unmodifiable" set can change. Always check whether the backing set is exposed. If it is, copy the elements into a new set before wrapping, or use Set.copyOf (Java 10+) to create an immutable set from an existing collection.

Set<String> original = new HashSet<>(); original.add("x"); Set<String> immutable = Set.copyOf(original); original.add("y"); // immutable is unaffected

Set.copyOf also rejects null and preserves the iteration order of the source only if the source has a defined order. For a HashSet, the order is not guaranteed.

Finally, remember that Set.of does not accept duplicate elements. If you pass duplicates, IllegalArgumentException is thrown at creation time. This is a useful safety check, but it means you cannot use Set.of to deduplicate a list that might contain duplicates without first processing it.

Using Immutable Sets as Constants and in Switch Expressions

Immutable sets are often used to define constants. For example, you can define a set of valid statuses and check membership without allocating a new set each time.

private static final Set<String> VALID_STATUSES = Set.of("NEW", "ACTIVE", "CLOSED"); public boolean isValidStatus(String status) { return VALID_STATUSES.contains(status); }

This avoids the overhead of creating a new set on each call and makes the allowed values explicit. In switch expressions (Java 14+), you cannot use a set directly, but you can use a set to validate input before switching.

Immutable sets also work well as part of a larger immutable data structure. If you store a set in a record or a final field, you know it will never change, which simplifies concurrency. Since an immutable set is inherently thread-safe, multiple threads can read it without synchronization.

Final Technical Consideration: Backing Data Exposure

When you create an immutable set from a collection, be careful about whether the source collection is later modified. Set.copyOf creates a new set and does not share state with the source. Collections.unmodifiableSet shares state. If you are building an API that returns a set, always copy the data before wrapping it to prevent external modification. This is a common source of subtle bugs in production systems where a collection is passed to multiple components and one component mutates it, causing unexpected behavior in another.

Prefer Set.of or Set.copyOf when you need guaranteed immutability. Reserve Collections.unmodifiableSet for cases where you intentionally want a view over a mutable set, such as when you need to provide a read-only view of an internal collection that you still control. Understanding this distinction will prevent many runtime surprises.

java immutable set: Practical Usage and Code Examples | RYUSLOG DEV