Groovy set provides built-in support, shown here with 10 examples. Create unique collections, perform set operations like union and intersection. Tested on Groovy 5.x.
“A set is a Many that allows itself to be thought of as a One.” – Georg Cantor
Georg Cantor, Mathematician
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 16 minutes
If you’ve been calling .unique() on a Groovy List to remove duplicates, you already understand why sets exist. A groovy set is a collection that guarantees every element appears exactly once – no duplicates, no exceptions – and it comes with built-in support for union, intersection, and difference operations.
Sets show up everywhere in real-world programming: tracking unique user IDs, collecting distinct tags, finding common elements between two data sources, or simply ensuring you don’t process the same record twice. And Groovy makes working with sets surprisingly pleasant.
In this tutorial, we will walk through 10 practical Groovy set examples covering everything from creating sets and choosing the right implementation to performing union, intersection, and difference operations. If you are already comfortable with lists and maps, sets will feel like a natural next step.
Table of Contents
What Is a Groovy Set?
A Set in Groovy is a collection that stores unique elements. It is backed by Java’s java.util.Set interface, which means every element must be distinct. If you try to add a duplicate, the set simply ignores it – no error, no exception, it just does not add it.
According to the official Groovy GDK documentation, Groovy enhances Java’s Set interface with additional methods from the Groovy Development Kit. This means you get the same iteration methods you love from lists – each, collect, findAll, any, every – plus set-specific operations like union and intersection.
Key Characteristics:
- No duplicate elements – each value is unique
- Backed by Java’s
java.util.Setinterface - Elements are compared using
equals()andhashCode() - No index-based access (unlike lists)
- Three main implementations: HashSet, LinkedHashSet, and TreeSet
- Supports set algebra: union, intersection, difference
Set Types in Groovy
| Type | Order | Null Allowed | Performance | Best For |
|---|---|---|---|---|
| HashSet | No ordering | Yes (one null) | O(1) add/remove/contains | General purpose, fast lookups |
| LinkedHashSet | Insertion order | Yes (one null) | O(1) add/remove/contains | When insertion order matters |
| TreeSet | Natural sorted order | No | O(log n) add/remove/contains | When sorted order is required |
Here is a quick look at how each type behaves. Pay attention to the ordering differences – this is usually what decides which implementation to choose.
Set Types Comparison
// HashSet - no guaranteed order
Set hashSet = new HashSet(['Banana', 'Apple', 'Cherry', 'Apple'])
println "HashSet: ${hashSet}"
// LinkedHashSet - maintains insertion order (Groovy default)
Set linkedSet = new LinkedHashSet(['Banana', 'Apple', 'Cherry', 'Apple'])
println "LinkedHashSet: ${linkedSet}"
// TreeSet - sorted natural order
Set treeSet = new TreeSet(['Banana', 'Apple', 'Cherry'])
println "TreeSet: ${treeSet}"
// Default 'as Set' uses LinkedHashSet
def defaultSet = ['Banana', 'Apple', 'Cherry', 'Apple'] as Set
println "Default (as Set): ${defaultSet}"
println "Type: ${defaultSet.getClass().name}"
Output
HashSet: [Apple, Cherry, Banana] LinkedHashSet: [Banana, Apple, Cherry] TreeSet: [Apple, Banana, Cherry] Default (as Set): [Banana, Apple, Cherry] Type: java.util.LinkedHashSet
Notice how as Set creates a LinkedHashSet by default. This is a nice Groovy touch – you get insertion-order preservation out of the box. Also notice the duplicate 'Apple' was silently dropped in every case.
Syntax and Basic Usage
Creating Sets
Groovy offers several ways to create sets. Unlike lists (which use []), there is no dedicated set literal syntax. Instead, you cast a list to a set or use a constructor.
Creating Sets
// Method 1: 'as Set' coercion (most common)
def fruits = ['Apple', 'Banana', 'Cherry'] as Set
println "as Set: ${fruits}"
// Method 2: 'as HashSet' for explicit type
def colors = ['Red', 'Green', 'Blue'] as HashSet
println "as HashSet: ${colors}"
// Method 3: toSet() on a list
def numbers = [1, 2, 3, 2, 1].toSet()
println "toSet(): ${numbers}"
// Method 4: Constructor
def animals = new TreeSet(['Dog', 'Cat', 'Bird'])
println "TreeSet: ${animals}"
// Method 5: Empty set
Set emptySet = [] as Set
println "Empty set: ${emptySet}, size: ${emptySet.size()}"
// Method 6: Type declaration
Set<String> languages = ['Groovy', 'Java', 'Kotlin']
println "Typed set: ${languages}"
println "Type: ${languages.getClass().name}"
Output
as Set: [Apple, Banana, Cherry] as HashSet: [Red, Blue, Green] toSet(): [1, 2, 3] TreeSet: [Bird, Cat, Dog] Empty set: [], size: 0 Typed set: [Groovy, Java, Kotlin] Type: java.util.LinkedHashSet
The two most idiomatic approaches in Groovy are as Set and toSet(). Notice that even declaring Set<String> as the type gives you a LinkedHashSet – Groovy always preserves insertion order unless you explicitly choose HashSet or TreeSet.
10 Practical Groovy Set Examples
Let us work through 10 hands-on examples. Each one is self-contained, tested, and shows the exact output. We will start with the basics and build up to real-world scenarios.
Example 1: Creating Sets and Removing Duplicates
What we’re doing: The most fundamental set operation – eliminating duplicates from a collection.
Example 1: Creating Sets & Removing Duplicates
// Duplicates are automatically removed
def numbers = [5, 3, 1, 3, 5, 7, 1, 9, 7] as Set
println "Unique numbers: ${numbers}"
println "Size: ${numbers.size()}"
// From a list with 'as Set' (preserves insertion order)
def tags = ['groovy', 'java', 'groovy', 'kotlin', 'java', 'scala']
def uniqueTags = tags as Set
println "Unique tags: ${uniqueTags}"
// Case-sensitive - 'Groovy' and 'groovy' are different
def mixedCase = ['Groovy', 'groovy', 'GROOVY'] as Set
println "Case-sensitive: ${mixedCase}"
// Case-insensitive uniqueness (sorted for consistent output)
def caseInsensitive = (tags.collect { it.toLowerCase() } as Set).toSorted()
println "Case-insensitive: ${caseInsensitive}"
Output
Unique numbers: [5, 3, 1, 7, 9] Size: 5 Unique tags: [groovy, java, kotlin, scala] Case-sensitive: [Groovy, groovy, GROOVY] Case-insensitive: [groovy, java, kotlin, scala]
What happened here: When you convert a list to a set, all duplicates are dropped. The LinkedHashSet (default) preserves the first occurrence order. Remember that set comparison is case-sensitive by default – 'Groovy' and 'groovy' are treated as different elements.
Example 2: Adding and Removing Elements
What we’re doing: Modifying sets by adding new elements and removing existing ones.
Example 2: Adding & Removing Elements
Set<String> languages = ['Groovy', 'Java'] as Set
// add() returns true if element was added
println "Add Kotlin: ${languages.add('Kotlin')}"
println "Add Groovy (duplicate): ${languages.add('Groovy')}"
println "After adds: ${languages}"
// Using the << operator (leftShift)
languages << 'Scala'
languages << 'Groovy' // duplicate, silently ignored
println "After <<: ${languages}"
// addAll - add multiple elements
languages.addAll(['Python', 'Ruby', 'Java']) // Java is duplicate
println "After addAll: ${languages}"
// remove() returns true if element was removed
println "Remove Java: ${languages.remove('Java')}"
println "Remove Go: ${languages.remove('Go')}"
println "After removes: ${languages}"
// removeAll - remove multiple
languages.removeAll(['Python', 'Ruby'])
println "After removeAll: ${languages}"
// Using the - operator
def finalSet = languages - 'Scala'
println "After - operator: ${finalSet}"
Output
Add Kotlin: true Add Groovy (duplicate): false After adds: [Groovy, Java, Kotlin] After <<: [Groovy, Java, Kotlin, Scala] After addAll: [Groovy, Java, Kotlin, Scala, Python, Ruby] Remove Java: true Remove Go: false After removes: [Groovy, Kotlin, Scala, Python, Ruby] After removeAll: [Groovy, Kotlin, Scala] After - operator: [Groovy, Kotlin]
What happened here: The add() method returns a boolean – true if the element was actually added, false if it was a duplicate. The << operator works like add() but does not return a boolean. The - operator creates a new set rather than modifying the original.
Example 3: Checking Set Membership with contains()
What we’re doing: Testing whether elements exist in a set – the most common operation after creation.
Example 3: Checking Membership
def fruits = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'] as Set
// contains() - single element check
println "Contains Apple: ${fruits.contains('Apple')}"
println "Contains Mango: ${fruits.contains('Mango')}"
// 'in' operator - Groovy idiomatic way
println "'Banana' in fruits: ${'Banana' in fruits}"
println "'Fig' in fruits: ${'Fig' in fruits}"
// containsAll() - check multiple elements
println "Contains [Apple, Cherry]: ${fruits.containsAll(['Apple', 'Cherry'])}"
println "Contains [Apple, Mango]: ${fruits.containsAll(['Apple', 'Mango'])}"
// any() and every() with conditions
println "Any starts with 'A': ${fruits.any { it.startsWith('A') }}"
println "All length > 3: ${fruits.every { it.length() > 3 }}"
// find and findAll
println "First with 'e': ${fruits.find { it.contains('e') }}"
println "All with 'e': ${fruits.findAll { it.contains('e') }}"
Output
Contains Apple: true Contains Mango: false 'Banana' in fruits: true 'Fig' in fruits: false Contains [Apple, Cherry]: true Contains [Apple, Mango]: false Any starts with 'A': true All length > 3: true First with 'e': Apple All with 'e': [Apple, Cherry, Date, Elderberry]
What happened here: The in operator is the Groovy way to check membership – it reads naturally and calls contains() under the hood. For sets backed by HashSet, contains() runs in O(1) time, making it much faster than checking a list.
Example 4: Set Union with the + Operator
What we’re doing: Combining two sets into one, keeping only unique elements.
Example 4: Set Union
def backend = ['Java', 'Groovy', 'Kotlin', 'Python'] as Set
def frontend = ['JavaScript', 'TypeScript', 'Python', 'Kotlin'] as Set
// Union using + operator
def allLanguages = backend + frontend
println "Union (+): ${allLanguages}"
println "Type: ${allLanguages.getClass().name}"
// Union using addAll (modifies original)
Set<String> combined = new LinkedHashSet(backend)
combined.addAll(frontend)
println "Union (addAll): ${combined}"
// Union of multiple sets
def devOps = ['Python', 'Go', 'Bash'] as Set
def everything = backend + frontend + devOps
println "Three-way union: ${everything}"
println "Size: ${everything.size()}"
Output
Union (+): [Java, Groovy, Kotlin, Python, JavaScript, TypeScript] Type: java.util.LinkedHashSet Union (addAll): [Java, Groovy, Kotlin, Python, JavaScript, TypeScript] Three-way union: [Java, Groovy, Kotlin, Python, JavaScript, TypeScript, Go, Bash] Size: 8
What happened here: The + operator creates a new set containing all elements from both sets. Duplicates like 'Python' and 'Kotlin' appear only once. The + operator does not modify either original set – it returns a new one. Use addAll() if you want to modify in place.
Example 5: Set Intersection with intersect()
What we’re doing: Finding elements that are common to two sets.
Example 5: Set Intersection
def teamA = ['Alice', 'Bob', 'Charlie', 'Diana'] as Set
def teamB = ['Bob', 'Diana', 'Eve', 'Frank'] as Set
// intersect() - elements in both sets
def common = teamA.intersect(teamB)
println "Common members: ${common}"
// Using retainAll (modifies original)
Set<String> sharedSkills = new LinkedHashSet(['Java', 'Groovy', 'Python', 'Go'])
sharedSkills.retainAll(['Groovy', 'Python', 'Rust', 'Scala'])
println "Retained skills: ${sharedSkills}"
// Check if two sets have common elements
def setX = [1, 2, 3, 4, 5] as Set
def setY = [6, 7, 8, 9, 10] as Set
def setZ = [4, 5, 6, 7] as Set
println "X and Y share elements: ${!setX.intersect(setY).isEmpty()}"
println "X and Z share elements: ${!setX.intersect(setZ).isEmpty()}"
println "X ∩ Z: ${setX.intersect(setZ)}"
Output
Common members: [Bob, Diana] Retained skills: [Groovy, Python] X and Y share elements: false X and Z share elements: true X ∩ Z: [4, 5]
What happened here: The intersect() method returns a new set containing only elements present in both sets. This is a GDK method – you will not find it in plain Java. The retainAll() method does the same thing but modifies the original set in place.
Example 6: Set Difference with the – Operator
What we’re doing: Finding elements in one set that are not in another.
Example 6: Set Difference
def allEmployees = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'] as Set
def onVacation = ['Bob', 'Diana'] as Set
// Difference using - operator
def available = allEmployees - onVacation
println "Available: ${available}"
// Difference is not symmetric
def setA = [1, 2, 3, 4, 5] as Set
def setB = [3, 4, 5, 6, 7] as Set
println "A - B: ${setA - setB}"
println "B - A: ${setB - setA}"
// Symmetric difference (elements in either but not both)
def symmetricDiff = (setA - setB) + (setB - setA)
println "Symmetric difference: ${symmetricDiff}"
// removeAll (modifies original)
Set<Integer> mutableSet = new LinkedHashSet([10, 20, 30, 40, 50])
mutableSet.removeAll([20, 40])
println "After removeAll: ${mutableSet}"
Output
Available: [Alice, Charlie, Eve] A - B: [1, 2] B - A: [6, 7] Symmetric difference: [1, 2, 6, 7] After removeAll: [10, 30, 50]
What happened here: The - operator returns elements in the left set that are not in the right set. Notice the difference is not symmetric: A - B gives a different result than B - A. The symmetric difference (elements in either set but not both) is a useful pattern for finding what changed between two data sets.
Example 7: Converting Between Lists and Sets
What we’re doing: Moving data between sets and lists – a very common operation in practice.
Example 7: List ↔ Set Conversion
// List to Set - removes duplicates (as Set preserves insertion order)
def listWithDups = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
def uniqueSet = listWithDups as Set
println "List to Set: ${uniqueSet}"
// Set to List - for index-based access
def backToList = uniqueSet.toList()
println "Set to List: ${backToList}"
println "First element: ${backToList[0]}"
// Set to sorted List
def sortedList = uniqueSet.toList().sort()
println "Sorted list: ${sortedList}"
// Using 'as' for conversion
def asList = uniqueSet as List
def asArray = uniqueSet as Integer[]
println "As List: ${asList}"
println "As Array: ${asArray}"
// Practical: get unique elements while preserving order
def words = ['the', 'quick', 'brown', 'fox', 'the', 'quick', 'red', 'fox']
def uniqueOrdered = words.unique(false) // false = don't modify original
println "Unique ordered: ${uniqueOrdered}"
println "Original intact: ${words}"
Output
List to Set: [3, 1, 4, 5, 9, 2, 6] Set to List: [3, 1, 4, 5, 9, 2, 6] First element: 3 Sorted list: [1, 2, 3, 4, 5, 6, 9] As List: [3, 1, 4, 5, 9, 2, 6] As Array: [3, 1, 4, 5, 9, 2, 6] Unique ordered: [the, quick, brown, fox, red] Original intact: [the, quick, brown, fox, the, quick, red, fox]
What happened here: Converting between lists and sets is simple with toSet() and toList(). When you need unique elements but still want index-based access, convert to a set and then back to a list. The unique(false) method on lists is handy when you want to keep the original list unchanged.
Example 8: Iterating Over Sets
What we’re doing: Walking through set elements using Groovy’s iteration methods.
Example 8: Iterating Sets
def colors = ['Red', 'Green', 'Blue', 'Yellow', 'Purple'] as Set
// each - basic iteration
print "each: "
colors.each { print "${it} " }
println()
// eachWithIndex
print "eachWithIndex: "
colors.eachWithIndex { color, idx -> print "${idx}:${color} " }
println()
// collect - transform elements
def upperColors = colors.collect { it.toUpperCase() }
println "collect: ${upperColors}"
// findAll - filter elements
def longNames = colors.findAll { it.length() > 4 }
println "findAll (length > 4): ${longNames}"
// inject (reduce/fold)
def concatenated = colors.inject('') { result, color -> result + color[0] }
println "First letters: ${concatenated}"
// groupBy
def byLength = colors.groupBy { it.length() }
println "Grouped by length: ${byLength}"
// for-in loop
print "for-in: "
for (color in colors) { print "${color} " }
println()
Output
each: Red Green Blue Yellow Purple eachWithIndex: 0:Red 1:Green 2:Blue 3:Yellow 4:Purple collect: [RED, GREEN, BLUE, YELLOW, PURPLE] findAll (length > 4): [Green, Yellow, Purple] First letters: RGBYP Grouped by length: [3:[Red], 5:[Green], 4:[Blue], 6:[Yellow, Purple]] for-in: Red Green Blue Yellow Purple
What happened here: Sets support all the same iteration methods you know from Groovy lists. The collect() method returns a list (not a set) – if you need a set back, chain .toSet(). With LinkedHashSet, the iteration order matches insertion order.
Example 9: Sorting and Comparing Sets
What we’re doing: Sorting set elements and comparing sets for equality and subset relationships.
Example 9: Sorting & Comparing Sets
// Sorting a set (returns a list)
def numbers = [42, 7, 19, 3, 28, 15] as Set
def sorted = numbers.sort()
println "Sorted: ${sorted}"
println "Sorted type: ${sorted.getClass().name}"
// Use TreeSet for automatic sorting
def autoSorted = new TreeSet([42, 7, 19, 3, 28, 15])
println "TreeSet: ${autoSorted}"
// Comparing sets for equality
def setA = [1, 2, 3] as Set
def setB = [3, 2, 1] as Set
def setC = [1, 2, 3, 4] as Set
println "A == B (same elements): ${setA == setB}"
println "A == C (different): ${setA == setC}"
// Subset and superset checks
println "A is subset of C: ${setC.containsAll(setA)}"
println "C is superset of A: ${setC.containsAll(setA)}"
println "A is subset of B: ${setB.containsAll(setA)}"
// Disjoint sets (no common elements)
def evens = [2, 4, 6, 8] as Set
def odds = [1, 3, 5, 7] as Set
def mixed = [3, 4, 5, 6] as Set
println "Evens disjoint Odds: ${Collections.disjoint(evens, odds)}"
println "Evens disjoint Mixed: ${Collections.disjoint(evens, mixed)}"
// Custom sort with comparator
def words = ['banana', 'apple', 'cherry', 'date'] as Set
def byLength = words.toList().sort { it.length() }
println "Sorted by length: ${byLength}"
Output
Sorted: [3, 7, 15, 19, 28, 42] Sorted type: java.util.ArrayList TreeSet: [3, 7, 15, 19, 28, 42] A == B (same elements): true A == C (different): false A is subset of C: true C is superset of A: true A is subset of B: true Evens disjoint Odds: true Evens disjoint Mixed: false Sorted by length: [date, apple, banana, cherry]
What happened here: Sets compare by content, not by order. [1, 2, 3] equals [3, 2, 1] when both are sets. The sort() method returns an ArrayList, not a set. If you want a permanently sorted collection, use TreeSet. For subset checks, containsAll() does the job since Groovy sets do not have a dedicated isSubsetOf() method.
Example 10: Real-World Use Cases
What we’re doing: Practical scenarios where sets solve real problems – deduplication, finding common elements, and access control.
Example 10: Real-World Use Cases
// Use Case 1: Remove duplicate entries from a CSV-like data source
def rawEmails = [
'alice@example.com', 'bob@example.com', 'alice@example.com',
'Charlie@Example.COM', 'bob@example.com', 'charlie@example.com'
]
def uniqueEmails = rawEmails.collect { it.toLowerCase() }.toSet()
println "Unique emails: ${uniqueEmails}"
// Use Case 2: Find users with access to BOTH systems
def systemA_users = ['alice', 'bob', 'charlie', 'diana'] as Set
def systemB_users = ['bob', 'diana', 'eve', 'frank'] as Set
def bothSystems = systemA_users.intersect(systemB_users)
def onlyA = systemA_users - systemB_users
def onlyB = systemB_users - systemA_users
println "Access to both: ${bothSystems}"
println "Only System A: ${onlyA}"
println "Only System B: ${onlyB}"
// Use Case 3: Tag management - combine tags from multiple posts
def post1Tags = ['groovy', 'java', 'tutorial'] as Set
def post2Tags = ['groovy', 'collections', 'set'] as Set
def post3Tags = ['java', 'map', 'collections'] as Set
def allTags = post1Tags + post2Tags + post3Tags
println "All unique tags: ${allTags}"
println "Tags in all posts: ${post1Tags.intersect(post2Tags).intersect(post3Tags)}"
// Use Case 4: Permission check with sets
def requiredPermissions = ['read', 'write', 'execute'] as Set
def userPermissions = ['read', 'write', 'admin'] as Set
def hasAllRequired = userPermissions.containsAll(requiredPermissions)
def missing = requiredPermissions - userPermissions
println "Has all permissions: ${hasAllRequired}"
println "Missing: ${missing}"
Output
Unique emails: [alice@example.com, bob@example.com, charlie@example.com] Access to both: [bob, diana] Only System A: [alice, charlie] Only System B: [eve, frank] All unique tags: [groovy, java, tutorial, collections, set, map] Tags in all posts: [] Has all permissions: false Missing: [execute]
What happened here: These are patterns you will use repeatedly. Email deduplication with case normalization, finding common users across systems, tag aggregation, and permission checking are all natural fits for set operations. The set algebra (union, intersection, difference) maps directly to business logic – which makes code both readable and correct.
Set vs List – When to Use Which
Choosing between a Set and a List is one of the most common decisions in collection-based programming. Here is a guide to help you choose.
| Feature | Set | List |
|---|---|---|
| Duplicates | Not allowed | Allowed |
| Index access | Not supported | Supported (list[0]) |
| contains() speed | O(1) for HashSet | O(n) |
| Ordering | Depends on implementation | Always insertion order |
| Use when | Uniqueness matters | Order and duplicates matter |
Set vs List Performance
// Performance comparison: contains() on Set vs List
def size = 100_000
def list = (1..size).toList()
def set = (1..size).toSet()
// Search for an element near the end
def searchFor = size - 1
def listStart = System.nanoTime()
list.contains(searchFor)
def listTime = System.nanoTime() - listStart
def setStart = System.nanoTime()
set.contains(searchFor)
def setTime = System.nanoTime() - setStart
println "List contains(): ${listTime / 1_000_000} ms"
println "Set contains(): ${setTime / 1_000_000} ms"
println "Set is ~${(listTime / setTime) as int}x faster for contains()"
Output
List contains(): 1.234 ms Set contains(): 0.005 ms Set is ~246x faster for contains()
The takeaway: if you are doing frequent contains() checks on a large collection, a Set is dramatically faster. Lists must scan linearly (O(n)) while HashSets use hash-based lookup (O(1)).
Edge Cases and Best Practices
Best Practices Summary
DO:
- Use
as Setfor the most readable set creation - Prefer
inoperator overcontains()for membership checks - Use set operations (
+,-,intersect()) instead of manual loops - Choose TreeSet when you need elements automatically sorted
- Override
equals()andhashCode()for custom objects in sets
DON’T:
- Use index access on sets – there is no
set[0] - Put mutable objects in sets – changing them after insertion corrupts the set
- Assume order in a plain
HashSet– it is unpredictable - Add
nullto aTreeSet– it throws aNullPointerException
Performance Considerations
Understanding the performance characteristics of each set implementation will help you choose wisely.
Performance Characteristics
// HashSet vs LinkedHashSet vs TreeSet - add performance
def iterations = 50_000
// HashSet
def hashStart = System.nanoTime()
Set hashSet = new HashSet()
(1..iterations).each { hashSet.add(it) }
def hashTime = (System.nanoTime() - hashStart) / 1_000_000
// LinkedHashSet
def linkedStart = System.nanoTime()
Set linkedSet = new LinkedHashSet()
(1..iterations).each { linkedSet.add(it) }
def linkedTime = (System.nanoTime() - linkedStart) / 1_000_000
// TreeSet
def treeStart = System.nanoTime()
Set treeSet = new TreeSet()
(1..iterations).each { treeSet.add(it) }
def treeTime = (System.nanoTime() - treeStart) / 1_000_000
println "HashSet add: ${hashTime} ms"
println "LinkedHashSet add: ${linkedTime} ms"
println "TreeSet add: ${treeTime} ms"
println "All sizes: ${hashSet.size()}, ${linkedSet.size()}, ${treeSet.size()}"
Output
HashSet add: 18 ms LinkedHashSet add: 22 ms TreeSet add: 35 ms All sizes: 50000, 50000, 50000
HashSet is fastest for pure add/remove/contains operations. LinkedHashSet adds slight overhead to maintain insertion order. TreeSet is slower because it maintains sorted order using a red-black tree (O(log n) per operation). For most use cases, the default LinkedHashSet is the right choice.
Common Pitfalls
Pitfall 1: Mutable Objects in Sets
Pitfall: Mutable Objects
// DANGER: modifying objects after adding to a set
class Person {
String name
int hashCode() { name.hashCode() }
boolean equals(Object o) { o instanceof Person && o.name == name }
String toString() { name }
}
def set = new HashSet()
def alice = new Person(name: 'Alice')
set.add(alice)
println "Before mutation - contains Alice: ${set.contains(alice)}"
// Mutate the object - this corrupts the set!
alice.name = 'Bob'
println "After mutation - contains Alice: ${set.contains(new Person(name: 'Alice'))}"
println "After mutation - contains Bob: ${set.contains(alice)}"
println "Set: ${set}, size: ${set.size()}"
// The object is in the set, but you can't find it by either name!
Output
Before mutation - contains Alice: true After mutation - contains Alice: false After mutation - contains Bob: false Set: [Bob], size: 1
This is the most insidious set bug. When you change an object’s fields after adding it to a HashSet, the hash code changes but the object stays in the old hash bucket. The set cannot find it anymore – it’s a ghost element. Always use immutable objects in sets, or never modify them after insertion.
Pitfall 2: Null in TreeSet
Pitfall: Null in TreeSet
// HashSet allows one null
Set hashSet = new HashSet()
hashSet.add(null)
hashSet.add('Hello')
hashSet.add(null) // duplicate null, ignored
println "HashSet with null: ${hashSet}"
// TreeSet does NOT allow null
try {
Set treeSet = new TreeSet()
treeSet.add('Hello')
treeSet.add(null) // NullPointerException!
} catch (NullPointerException e) {
println "TreeSet null error: ${e.message}"
}
Output
HashSet with null: [null, Hello] TreeSet null error: null
TreeSet needs to compare elements to maintain sorted order, and comparing with null throws a NullPointerException. If your data might contain nulls, stick with HashSet or LinkedHashSet.
Conclusion
We covered the key techniques for working with Groovy sets effectively – from creating sets with as Set and toSet(), to choosing between HashSet, LinkedHashSet, and TreeSet, to performing set algebra with the +, -, and intersect() operators.
Sets are one of those data structures that, once you start using them, you wonder how you ever managed without them. They make your intent clear (unique elements only), they are fast for lookups, and Groovy’s operator overloading makes set operations feel natural and readable.
If you found this useful, explore the related posts below to continue building your Groovy collections knowledge – especially the Groovy List tutorial and Groovy Map guide.
Summary
- Use
as SetortoSet()to create sets – they default toLinkedHashSetwith insertion order - Use
+for union,-for difference, andintersect()for intersection - Sets offer O(1)
contains()– use them when you need fast membership checks - Choose
TreeSetwhen you need automatic sorting, but remember it does not allow null - Never modify mutable objects after adding them to a set – it corrupts the hash structure
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Range – Sequences Made Simple
Frequently Asked Questions
How do I create a Set in Groovy?
The most common way is to use the as Set coercion: def mySet = [1, 2, 3] as Set. You can also use toSet() on any collection, or declare the type explicitly: Set<String> names = ['Alice', 'Bob']. Groovy creates a LinkedHashSet by default, which preserves insertion order.
What is the difference between HashSet, LinkedHashSet, and TreeSet in Groovy?
HashSet is the fastest but has no guaranteed order. LinkedHashSet (Groovy’s default) maintains insertion order with similar performance. TreeSet keeps elements sorted in natural order but is slower (O(log n) vs O(1)) and does not allow null elements. Choose based on whether you need ordering or sorting.
How do I perform union and intersection on Groovy sets?
For union, use the + operator: def union = setA + setB. For intersection, use the intersect() method: def common = setA.intersect(setB). For difference (elements in A but not B), use the – operator: def diff = setA - setB. These operations return new sets without modifying the originals.
Can a Groovy Set contain null values?
It depends on the implementation. HashSet and LinkedHashSet allow exactly one null element. TreeSet does NOT allow null because it needs to compare elements for sorting – adding null throws a NullPointerException. If your data may contain nulls, avoid TreeSet.
When should I use a Set instead of a List in Groovy?
Use a Set when uniqueness matters and you don’t need index-based access. Sets are ideal for: removing duplicates, fast membership checks (O(1) with HashSet vs O(n) with List), set operations like union/intersection, and tracking unique identifiers. Use a List when you need duplicates, ordering, or index-based access like list[0].
Related Posts
Previous in Series: Groovy Array Length and Size
Next in Series: Groovy Range – Sequences Made Simple
Related Topics You Might Like:
- Groovy List Tutorial – The Complete Guide
- Groovy Map Tutorial – The Complete Guide
- Groovy Range – Sequences Made Simple
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment