10 Practical Groovy Set Examples for Unique Collections

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.

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.Set interface
  • Elements are compared using equals() and hashCode()
  • No index-based access (unlike lists)
  • Three main implementations: HashSet, LinkedHashSet, and TreeSet
  • Supports set algebra: union, intersection, difference

Set Types in Groovy

TypeOrderNull AllowedPerformanceBest For
HashSetNo orderingYes (one null)O(1) add/remove/containsGeneral purpose, fast lookups
LinkedHashSetInsertion orderYes (one null)O(1) add/remove/containsWhen insertion order matters
TreeSetNatural sorted orderNoO(log n) add/remove/containsWhen 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.

FeatureSetList
DuplicatesNot allowedAllowed
Index accessNot supportedSupported (list[0])
contains() speedO(1) for HashSetO(n)
OrderingDepends on implementationAlways insertion order
Use whenUniqueness mattersOrder 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 Set for the most readable set creation
  • Prefer in operator over contains() for membership checks
  • Use set operations (+, -, intersect()) instead of manual loops
  • Choose TreeSet when you need elements automatically sorted
  • Override equals() and hashCode() 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 null to a TreeSet – it throws a NullPointerException

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 Set or toSet() to create sets – they default to LinkedHashSet with insertion order
  • Use + for union, - for difference, and intersect() for intersection
  • Sets offer O(1) contains() – use them when you need fast membership checks
  • Choose TreeSet when 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].

Previous in Series: Groovy Array Length and Size

Next in Series: Groovy Range – Sequences Made Simple

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *