1. Union operation (set union)
In Java, sets are represented by the Set<T> interface. Unlike lists (List), sets guarantee element uniqueness and generally do not care about order (unless you use special implementations such as LinkedHashSet). The most popular implementations are HashSet and TreeSet. Their main job is to quickly determine whether an element is present and ensure there are no duplicates.
When do you need sets?
- When uniqueness matters: for example, a list of all unique users who visited a website.
- When you need fast membership checks: the contains method on HashSet typically runs in constant time.
- When you need to perform common set operations: union, intersection, difference.
Union is the combination of two or more sets: the result contains all elements from both source sets (without duplicates).
Practical example
Suppose we have two sets of students who attend the clubs “Robotics” and “Programming”:
Set<String> robotics = Set.of("Alice", "Bob", "Victoria");
Set<String> programming = Set.of("Victoria", "George", "Diana");
We need to get the set of all students who attend at least one club.
Solution via the Stream API
The most straightforward way is to concatenate both streams and collect them into a Set:
Set<String> all = Stream.concat(
robotics.stream(),
programming.stream()
).collect(Collectors.toSet());
System.out.println(all); // [Alice, Bob, Victoria, George, Diana]
Explanation:
- Stream.concat concatenates two streams.
- collect(Collectors.toSet()) collects elements into a set (automatically removing duplicates).
Alternative: more than two sets
If you have three or more clubs, use Stream.of and flatMap:
Set<String> math = Set.of("Eugene", "Victoria", "Bob");
Set<String> all = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
System.out.println(all); // [Alice, Bob, Victoria, George, Diana, Eugene]
Why Set?
Because Set automatically removes duplicates. If you collect into a List, the same names will appear multiple times.
2. Intersection operation (set intersection)
Intersection is the set of elements that are present in both sets at the same time.
Practical example
Find students who attend both “Robotics” and “Programming”:
Set<String> robotics = Set.of("Alice", "Bob", "Victoria");
Set<String> programming = Set.of("Victoria", "George", "Diana");
Solution via the Stream API:
Set<String> both = robotics.stream()
.filter(programming::contains)
.collect(Collectors.toSet());
System.out.println(both); // [Victoria]
Explanation:
We iterate over all “Robotics” participants and keep only those who are also in “Programming”. The result is a set of names that occur in both clubs.
Alternative (without the Stream API)
You can use the built-in retainAll method (it mutates the current set):
Set<String> intersection = new HashSet<>(robotics);
intersection.retainAll(programming);
System.out.println(intersection); // [Victoria]
But within the Stream API topic, we focus on streams.
3. Difference operation (set difference)
Difference is the set of elements in the first set that are not in the second set.
Practical example
Find students who attend only “Robotics” but not “Programming”:
Set<String> robotics = Set.of("Alice", "Bob", "Victoria");
Set<String> programming = Set.of("Victoria", "George", "Diana");
Solution via the Stream API:
Set<String> onlyRobotics = robotics.stream()
.filter(name -> !programming.contains(name))
.collect(Collectors.toSet());
System.out.println(onlyRobotics); // [Alice, Bob]
Explanation:
We filter the “Robotics” participants, keeping only those who are not in “Programming”.
Alternative (without the Stream API)
Set<String> difference = new HashSet<>(robotics);
difference.removeAll(programming);
System.out.println(difference); // [Alice, Bob]
4. Practical tasks: processing student lists
Task 1: Find students who attend only one club
We need to find those who attend only “Robotics” or only “Programming”, but not both. This is a symmetric difference (xor for sets):
Set<String> onlyOne = Stream.concat(
robotics.stream().filter(name -> !programming.contains(name)),
programming.stream().filter(name -> !robotics.contains(name))
).collect(Collectors.toSet());
System.out.println(onlyOne); // [Alice, Bob, George, Diana]
Task 2: List all unique students from several clubs
Set<String> all = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
System.out.println(all); // [Alice, Bob, Victoria, George, Diana, Eugene]
Task 3: Find students who do not attend any club
Suppose we have a list of all students in the class:
Set<String> allStudents = Set.of("Alice", "Bob", "Victoria", "George", "Diana", "Eugene", "Ian", "Kate");
We need to find those who do not attend any club:
Set<String> attendees = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
Set<String> notInAny = allStudents.stream()
.filter(name -> !attendees.contains(name))
.collect(Collectors.toSet());
System.out.println(notInAny); // [Ian, Kate]
5. Important notes: equals, hashCode, and performance
Why is it important to implement equals and hashCode correctly?
All operations with sets (Set) depend on correct implementations of equals and hashCode. If you store objects of your own class (for example, Student), you must override these methods; otherwise, comparisons will behave incorrectly.
Example:
class Student {
String name;
int age;
// Don't forget to override equals and hashCode!
}
If you don’t, two students with the same name and age will be considered different objects by Set.
Why use Set rather than List?
- The contains operation on a Set is fast (usually constant time).
- In a List, searching for an element is linear, which can be critical for large collections.
- For set operations (union, intersection, difference), Set is much more efficient and semantically appropriate.
6. Common mistakes when working with set operations
Mistake #1: Using List instead of Set for set operations. If you collect elements into a List, duplicates are not removed, and contains is slow. For union/intersection/difference, use a Set.
Mistake #2: equals/hashCode not implemented for objects. If you store objects of your own class in a Set but don’t override equals and hashCode, intersection and difference will behave “strangely” — objects that are logically equal will not be considered equal.
Mistake #3: Modifying a collection during a stream. If you try to modify the source Set directly inside a stream (for example, adding or removing elements), you’ll get a ConcurrentModificationException. Always work with a new set.
Mistake #4: Unintended loss of order. HashSet does not guarantee element order. If order matters, use LinkedHashSet or TreeSet.
Mistake #5: Using Stream.concat for more than two collections. Stream.concat concatenates only two streams. For more, use Stream.of(...) and flatMap.
Mistake #6: Issues with null. Sets don’t play well with null values, especially if you use Set.of(...) — it does not allow null. To handle null, use other implementations or filter values in advance.
GO TO FULL VERSION