Back to Blog
Java

Java Set of: Creating and Using Sets

java set of: Learn how to create, populate, and iterate over a Set in Java, including the differences between HashSet, TreeSet, and LinkedHashSet.

JavaSet interfaceHashSetTreeSetLinkedHashSetCollections
Illustration of a Java Set collection showing unique elements with a focus on order and uniqueness

When you need a collection that guarantees unique elements, the Set interface is the standard choice. The phrase "java set of" often refers to creating a set of elements, and Java 9 introduced Set.of() for convenient immutable sets. This article covers the practical ways to work with sets in Java, from the static factory method to the classic mutable implementations, and explains the tradeoffs you need to consider when choosing one.

Creating a Set with Set.of()

Java 9 added the Set.of() static factory method, which creates an immutable set containing the specified elements. This is the most concise way to create a set when you know the elements at compile time and do not need to modify the collection later.

Set<String> fruits = Set.of("apple", "banana", "cherry");

The method is overloaded to accept zero or more arguments, and it returns a highly optimized, unmodifiable set. There are several important constraints:

  • The set does not allow null elements. Passing null throws a NullPointerException.
  • Duplicate elements are rejected at creation time. If you pass two identical elements, the method throws IllegalArgumentException.
  • The iteration order is unspecified and may change between runs, so you should not rely on any particular order.

Because the set is immutable, any attempt to add, remove, or clear it throws UnsupportedOperationException. This is a deliberate design choice that makes the set safe for sharing across threads and suitable for use as a constant.

Using HashSet, TreeSet, and LinkedHashSet

For mutable sets, Java provides three main implementations in the java.util package. Each has distinct characteristics that affect ordering and performance.

HashSet

HashSet is backed by a hash table and offers constant-time average performance for add, remove, and contains. It makes no guarantees about iteration order; the order can change when the set is resized. This is the most commonly used set implementation because it is fast and simple.

Set<Integer> numbers = new HashSet<>(); numbers.add(3); numbers.add(1); numbers.add(2);

TreeSet

TreeSet stores elements in a red-black tree and keeps them sorted according to their natural ordering or a custom Comparator. All operations run in O(log n) time, which is slower than HashSet but still acceptable for most collections. The iteration order is the sorted order, which is useful when you need to process elements in a defined sequence.

Set<String> words = new TreeSet<>(); words.add("banana"); words.add("apple"); words.add("cherry"); // Iteration order: apple, banana, cherry

LinkedHashSet

LinkedHashSet sits between HashSet and TreeSet. It uses a hash table for storage but maintains a doubly-linked list of entries to preserve insertion order. This gives you predictable iteration order without the logarithmic cost of a tree. Performance is similar to HashSet for basic operations, but there is a small memory overhead for the linked list.

Set<String> ordered = new LinkedHashSet<>(); ordered.add("first"); ordered.add("second"); ordered.add("third"); // Iteration order: first, second, third

Adding and Removing Elements

Mutable sets expose the add, remove, and clear methods defined in the Collection interface. The add method returns true if the element was not already present, and false if it was a duplicate. This return value is useful when you need to detect whether a new element was actually inserted.

Set<String> names = new HashSet<>(); boolean addedFirst = names.add("Alice"); // true boolean addedSecond = names.add("Alice"); // false

Removing an element returns true if the set contained the element and removed it. The removeAll and retainAll methods allow bulk operations, and clear removes everything. For immutable sets created with Set.of(), these methods throw UnsupportedOperationException.

Iterating Over a Set

Sets do not have an index-based accessor like List, so iteration is done through an Iterator, an enhanced for loop, or a stream. The order depends on the implementation, as described earlier.

Set<String> colors = new LinkedHashSet<>(); colors.add("red"); colors.add("green"); colors.add("blue"); for (String color : colors) { System.out.println(color); }

You can also use the forEach method with a lambda expression:

colors.forEach(color -> System.out.println(color));

If you need to transform the set into a list or another collection, you can use streams:

List<String> uppercase = colors.stream() .map(String::toUpperCase) .collect(Collectors.toList());

Streams are particularly powerful when you need to filter, map, or aggregate elements from a set.

Performance and Memory Considerations

The choice of set implementation has direct performance implications. HashSet offers average O(1) time for add, remove, and contains, but its iteration order is unpredictable. TreeSet guarantees O(log n) operations and sorted iteration, but the constant factors are higher due to tree traversal. LinkedHashSet provides O(1) operations and insertion order, but uses slightly more memory because of the linked list.

For small sets (a few dozen elements), the performance differences are negligible. The decision should be driven by ordering requirements rather than micro-optimization. If you need sorted order, use TreeSet; if you need insertion order, use LinkedHashSet; otherwise, HashSet is the default choice.

Memory usage also varies. HashSet uses a hash table with a load factor that causes it to allocate more buckets than elements. TreeSet stores each element in a tree node with references to left and right children, which adds overhead. LinkedHashSet adds two references per entry for the linked list. In practice, the differences are small unless you are storing millions of elements.

Common Pitfalls with Sets

One frequent mistake is using mutable objects as set elements without implementing equals() and hashCode() correctly. If an object's fields change after it is added to a HashSet, the hash code changes, and the element becomes unreachable in the set. This can lead to memory leaks and unexpected behavior. Always use immutable objects as set elements when possible, or ensure that the object's state does not affect its identity.

Another pitfall is relying on iteration order without knowing the implementation. If you use HashSet and later switch to TreeSet for sorting, code that assumes a particular order will break. Always document the ordering contract of your set when it matters.

Null handling is also implementation-specific. HashSet and LinkedHashSet allow at most one null element, while TreeSet throws NullPointerException when you try to add null because it needs to compare elements. Set.of() rejects null entirely.

Finally, be aware that Set is not thread-safe. If multiple threads access a set concurrently, you must synchronize externally or use a concurrent implementation like ConcurrentSkipListSet or wrap the set with Collections.synchronizedSet(). The immutable set from Set.of() is inherently thread-safe because it cannot be modified.

Choosing the Right Set Implementation

Selecting the right set implementation depends on your specific requirements:

  • Use Set.of() when the set is constant and you want a compact, immutable, and thread-safe collection.
  • Use HashSet when you need fast operations and do not care about iteration order.
  • Use LinkedHashSet when you want to preserve insertion order and still have near-constant-time operations.
  • Use TreeSet when you need sorted iteration or when you need to perform range-based queries like subSet or headSet.

If you are working with a large set and need to frequently check membership, HashSet is usually the best choice. If you need to iterate in a specific order, TreeSet or LinkedHashSet will save you from sorting the elements manually. For most application code, HashSet is the default because it offers the best balance of speed and simplicity.

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