Checking whether a Groovy list contains a value, removing elements without hitting the index-versus-value trap, and stripping duplicates: these three jobs come up in nearly every script. This guide works through all of them with 10 tested examples on Groovy 5.0.6.
“The art of programming is the art of organizing complexity – and lists that contain duplicates and stale entries are complexity waiting to bite you.”
Last Updated: July 2026 | Tested on: Groovy 5.0.6, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 18 minutes
Once you know how to create and iterate Groovy lists (covered in our Groovy List Tutorial), the next thing you need to master is finding elements, removing them, and cleaning up duplicates. These three operations come up constantly in real-world code – validating user input, filtering API responses, cleaning CSV imports, and deduplicating database results.
Groovy gives you a surprisingly rich toolkit for all of this. Beyond the basic contains() method inherited from Java, you get the in operator, find(), any(), every(), and several powerful removal methods that Java’s standard collections don’t offer. And for deduplication, unique() with closures is genuinely elegant.
In this post, you will see 10 tested examples covering groovy list contains checks, groovy remove from list operations, and deduplication strategies. Every example runs on Groovy 5.0.6 with verified output. Together, they give you the complete picture of how to search, clean, and deduplicate lists in Groovy.
The diagram below shows how Groovy decides which removal method to run based on what you pass in, a position, a single value, a condition, or a set of values, and whether that choice mutates your original list or hands back a new one:
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
Table of Contents
Understanding List Contains in Groovy
Checking whether a list contains a specific element is one of the most frequent operations in any language. In Java, you call list.contains(element) and that is about it. Groovy keeps that method but layers several more approaches on top of it.
According to the official Groovy GDK documentation on filtering and searching, the GDK adds methods like find(), findAll(), any(), and every() to all Collection types. These let you search with conditions, not just exact matches.
Key Methods for Checking Containment:
contains(element)– returns true if the list has the exact elementcontainsAll(collection)– returns true if the list has all elements from another collectionelement in list– theinoperator, syntactic sugar for contains()find { condition }– returns the first element matching a closure, or nullany { condition }– returns true if at least one element matchesevery { condition }– returns true if all elements match
Syntax and Method Overview
| Method | Purpose | Returns | Modifies List? |
|---|---|---|---|
contains() | Check for exact element | boolean | No |
containsAll() | Check for multiple elements | boolean | No |
in operator | Readable containment check | boolean | No |
find{} | First matching element | element or null | No |
any{} | At least one matches? | boolean | No |
every{} | All match? | boolean | No |
remove(index) | Remove by position | removed element | Yes |
removeElement(value) | Remove first occurrence | boolean | Yes |
removeAll{} | Remove all matching | boolean | Yes |
removeIf{} | Remove if predicate true | boolean | Yes |
retainAll{} | Keep only matching | boolean | Yes |
unique() | Remove duplicates in-place | the list | Yes |
unique(false) | Deduplicate without modifying | new list | No |
toSet() | Convert to Set (no dupes) | Set | No |
minus operator (-) | Subtract elements | new list | No |
10 Practical Examples
Let us walk through 10 tested examples, starting with containment checks, moving into removal operations, and finishing with deduplication techniques. Each example includes the Groovy code and verified output.
Example 1: contains() and containsAll()
What we’re doing: Using the standard contains() and containsAll() methods to check if a list holds specific elements.
Example 1: contains() and containsAll()
def fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry']
// Basic contains check
println "Contains 'banana': ${fruits.contains('banana')}"
println "Contains 'mango': ${fruits.contains('mango')}"
// Check for multiple elements at once
println "Has apple AND cherry: ${fruits.containsAll(['apple', 'cherry'])}"
println "Has apple AND mango: ${fruits.containsAll(['apple', 'mango'])}"
// Case sensitivity matters
println "Contains 'Apple': ${fruits.contains('Apple')}"
// Works with numbers too
def numbers = [10, 20, 30, 40, 50]
println "Contains 30: ${numbers.contains(30)}"
println "Contains 35: ${numbers.contains(35)}"
Output
Contains 'banana': true Contains 'mango': false Has apple AND cherry: true Has apple AND mango: false Contains 'Apple': false Contains 30: true Contains 35: false
What happened here: The contains() method performs an exact match using equals(). It is case-sensitive for strings. The containsAll() method takes a collection and returns true only if every element in that collection is found in the list. Both methods are inherited from Java’s java.util.Collection interface.
Example 2: The in Operator and find()
What we’re doing: Using Groovy’s in operator for readable containment checks and find() to locate elements by condition.
Example 2: in Operator and find()
def languages = ['Groovy', 'Java', 'Kotlin', 'Scala', 'Clojure']
// The 'in' operator - syntactic sugar for contains()
println "'Groovy' in list: ${'Groovy' in languages}"
println "'Python' in list: ${'Python' in languages}"
// Use 'in' in if statements - very readable
if ('Java' in languages) {
println "Java is in the list!"
}
// find() - returns first element matching condition
def found = languages.find { it.startsWith('K') }
println "First starting with K: ${found}"
// find() returns null when nothing matches
def notFound = languages.find { it.startsWith('R') }
println "First starting with R: ${notFound}"
// find() with more complex conditions
def numbers = [3, 7, 12, 18, 25, 30]
def firstEvenOver10 = numbers.find { it > 10 && it % 2 == 0 }
println "First even number over 10: ${firstEvenOver10}"
Output
'Groovy' in list: true 'Python' in list: false Java is in the list! First starting with K: Kotlin First starting with R: null First even number over 10: 12
What happened here: The in operator is Groovy syntactic sugar that calls isCase() under the hood, which for lists delegates to contains(). It reads much more naturally than list.contains(element). The find() method accepts a closure and returns the first element for which the closure returns true. If nothing matches, it returns null.
Example 3: any() and every()
What we’re doing: Using any() to check if at least one element matches a condition, and every() to check if all elements match.
Example 3: any() and every()
def scores = [85, 92, 78, 95, 88, 72, 91]
// any() - does at least one element match?
println "Any score above 90: ${scores.any { it > 90 }}"
println "Any score below 50: ${scores.any { it < 50 }}"
// every() - do ALL elements match?
println "All scores above 70: ${scores.every { it > 70 }}"
println "All scores above 80: ${scores.every { it > 80 }}"
// Practical: validate all entries
def emails = ['alice@example.com', 'bob@test.org', 'charlie@demo.net']
def allValidEmails = emails.every { it.contains('@') && it.contains('.') }
println "All valid emails: ${allValidEmails}"
// Mix with strings
def words = ['Groovy', 'Great', 'Good', 'Go']
println "Any word longer than 5 chars: ${words.any { it.length() > 5 }}"
println "All words start with G: ${words.every { it.startsWith('G') }}"
Output
Any score above 90: true Any score below 50: false All scores above 70: true All scores above 80: false All valid emails: true Any word longer than 5 chars: true All words start with G: true
What happened here: The any() method short-circuits – it stops iterating the moment it finds a match, so it is efficient on large lists. The every() method also short-circuits, stopping as soon as it finds an element that does not match. These methods are GDK additions and are much cleaner than writing manual loops with boolean flags.
Example 4: Remove by Index and Remove by Value
What we’re doing: Understanding the critical difference between remove(int index) and removeElement(Object value) in Groovy.
Example 4: Remove by Index vs Value
// Remove by index
def colors = ['red', 'green', 'blue', 'yellow', 'purple']
println "Original: ${colors}"
def removed = colors.remove(1) // removes element at index 1
println "Removed index 1: '${removed}'"
println "After remove(1): ${colors}"
// Remove by value using removeElement()
def cities = ['Paris', 'London', 'Tokyo', 'Paris', 'Berlin']
println "\nOriginal: ${cities}"
boolean wasRemoved = cities.removeElement('Paris') // removes FIRST occurrence
println "removeElement('Paris') returned: ${wasRemoved}"
println "After removeElement: ${cities}"
// The gotcha with integers!
def nums = [10, 20, 30, 40, 50]
println "\nOriginal numbers: ${nums}"
// remove(2) removes element at INDEX 2, not the value 2!
def removedNum = nums.remove(2)
println "remove(2) removed value: ${removedNum}"
println "After remove(2): ${nums}"
// To remove the value 2 from an integer list, use removeElement
def nums2 = [1, 2, 3, 4, 5]
nums2.removeElement(2)
println "\nAfter removeElement(2): ${nums2}"
Output
Original: [red, green, blue, yellow, purple]
Removed index 1: 'green'
After remove(1): [red, blue, yellow, purple]
Original: [Paris, London, Tokyo, Paris, Berlin]
removeElement('Paris') returned: true
After removeElement: [London, Tokyo, Paris, Berlin]
Original numbers: [10, 20, 30, 40, 50]
remove(2) removed value: 30
After remove(2): [10, 20, 40, 50]
After removeElement(2): [1, 3, 4, 5]
What happened here: This is one of the most common gotchas when working with Groovy lists. When you call remove(2) on a list of integers, Groovy calls remove(int index), not remove(Object). This means it removes the element at position 2, not the element with value 2. To remove by value from an integer list, always use removeElement(). This method was added by Groovy specifically to resolve this ambiguity.
Example 5: removeAll() and removeIf()
What we’re doing: Removing multiple elements at once using removeAll() with a closure and removeIf() with a predicate.
Example 5: removeAll() and removeIf()
// removeAll with a collection
def animals = ['cat', 'dog', 'bird', 'fish', 'hamster', 'snake']
println "Original: ${animals}"
animals.removeAll(['bird', 'snake', 'lizard'])
println "After removeAll(['bird','snake','lizard']): ${animals}"
// removeAll with a closure
def numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
println "\nOriginal: ${numbers}"
numbers.removeAll { it % 2 == 0 } // remove all even numbers
println "After removing evens: ${numbers}"
// removeIf with a predicate (Java 8+ style)
def temps = [72.5, 68.3, 95.1, 101.4, 55.8, 88.9, 110.2]
println "\nTemperatures: ${temps}"
temps.removeIf { it > 100 }
println "After removing > 100: ${temps}"
// removeAll with a collection of specific values
def tags = ['java', 'groovy', 'python', 'groovy', 'java', 'kotlin']
println "\nTags: ${tags}"
tags.removeAll(['java'])
println "After removing all 'java': ${tags}"
Output
Original: [cat, dog, bird, fish, hamster, snake] After removeAll(['bird','snake','lizard']): [cat, dog, fish, hamster] Original: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] After removing evens: [1, 3, 5, 7, 9] Temperatures: [72.5, 68.3, 95.1, 101.4, 55.8, 88.9, 110.2] After removing > 100: [72.5, 68.3, 95.1, 55.8, 88.9] Tags: [java, groovy, python, groovy, java, kotlin] After removing all 'java': [groovy, python, groovy, kotlin]
What happened here: The removeAll() method is versatile. When you pass it a collection, it removes every occurrence of each element in that collection. When you pass it a closure, it removes every element for which the closure returns true. The removeIf() method works the same way as the closure-based removeAll() but comes from Java 8’s Collection API. Both modify the original list in place and return a boolean indicating whether the list changed.
Example 6: retainAll() and the Minus Operator
What we’re doing: Keeping only specific elements with retainAll() and using the minus operator to create new lists without certain elements.
Example 6: retainAll() and Minus Operator
// retainAll - keep only elements matching a condition
def scores = [45, 82, 91, 33, 76, 88, 95, 60, 71]
println "Original scores: ${scores}"
scores.retainAll { it >= 70 }
println "After retainAll(>= 70): ${scores}"
// retainAll with a collection
def allowed = ['admin', 'editor', 'viewer']
def userRoles = ['admin', 'editor', 'superuser', 'viewer', 'guest']
println "\nUser roles: ${userRoles}"
userRoles.retainAll(allowed)
println "After retainAll(allowed): ${userRoles}"
// Minus operator - creates a NEW list (original unchanged)
def original = ['a', 'b', 'c', 'd', 'e', 'b', 'c']
println "\nOriginal: ${original}"
def result1 = original - 'b'
println "original - 'b': ${result1}"
def result2 = original - ['b', 'c']
println "original - ['b','c']: ${result2}"
println "Original unchanged: ${original}"
// Minus operator removes ALL occurrences
def dupes = [1, 2, 3, 2, 4, 2, 5]
println "\nWith dupes: ${dupes}"
println "dupes - [2]: ${dupes - [2]}"
Output
Original scores: [45, 82, 91, 33, 76, 88, 95, 60, 71] After retainAll(>= 70): [82, 91, 76, 88, 95, 71] User roles: [admin, editor, superuser, viewer, guest] After retainAll(allowed): [admin, editor, viewer] Original: [a, b, c, d, e, b, c] original - 'b': [a, c, d, e, c] original - ['b','c']: [a, d, e] Original unchanged: [a, b, c, d, e, b, c] With dupes: [1, 2, 3, 2, 4, 2, 5] dupes - [2]: [1, 3, 4, 5]
What happened here: The retainAll() method is the opposite of removeAll() – it keeps only the elements that match and throws away everything else. It modifies the list in place. The minus operator (-) creates a brand new list with all occurrences of the specified elements removed. The original list stays untouched, which makes the minus operator the safer choice when you do not want side effects.
Example 7: unique() for Deduplication
What we’re doing: Removing duplicate elements using unique() with and without modifying the original list.
Example 7: unique() Deduplication
// unique() modifies the list in place (default behavior)
def colors = ['red', 'blue', 'green', 'red', 'blue', 'yellow', 'red']
println "Original: ${colors}"
colors.unique()
println "After unique(): ${colors}"
// unique(false) returns a NEW list without modifying original
def numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
println "\nOriginal numbers: ${numbers}"
def uniqueNums = numbers.unique(false)
println "unique(false) result: ${uniqueNums}"
println "Original unchanged: ${numbers}"
// unique() preserves first occurrence order
def letters = ['c', 'a', 'b', 'a', 'c', 'b', 'd']
println "\nOriginal: ${letters}"
println "unique(false): ${letters.unique(false)}"
// Works with mixed types too
def mixed = [1, '1', 1, 'one', 1.0, '1']
println "\nMixed types: ${mixed}"
println "unique(false): ${mixed.unique(false)}"
Output
Original: [red, blue, green, red, blue, yellow, red] After unique(): [red, blue, green, yellow] Original numbers: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] unique(false) result: [3, 1, 4, 5, 9, 2, 6] Original unchanged: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] Original: [c, a, b, a, c, b, d] unique(false): [c, a, b, d] Mixed types: [1, 1, 1, one, 1.0, 1] unique(false): [1, 1, one]
What happened here: By default, unique() modifies the list in place and returns a reference to the same list. If you pass false as the first argument, it returns a new deduplicated list and leaves the original alone. Notice that unique() preserves the order of first occurrence. Also note how Groovy treats 1 (Integer), '1' (String), and 1.0 (BigDecimal) – the numeric types 1 and 1.0 are equal by Groovy’s comparison rules, so unique() keeps only the first one.
Example 8: unique() with Closure and toSet()
What we’re doing: Using unique() with a closure for custom deduplication logic and toSet() for quick conversion.
Example 8: unique() with Closure and toSet()
// unique with closure - deduplicate by custom criteria
def names = ['Alice', 'ALICE', 'alice', 'Bob', 'BOB', 'Charlie']
println "Original: ${names}"
// Case-insensitive dedup using a comparator closure
def uniqueNames = names.unique(false) { a, b -> a.toLowerCase() <=> b.toLowerCase() }
println "Case-insensitive unique: ${uniqueNames}"
// unique with a single-arg closure (grouping key)
def words = ['hello', 'world', 'hi', 'help', 'wonder', 'heap']
def uniqueByFirstLetter = words.unique(false) { it[0] }
println "\nUnique by first letter: ${uniqueByFirstLetter}"
// Deduplicate objects by a property
def people = [
[name: 'Alice', dept: 'Engineering'],
[name: 'Bob', dept: 'Marketing'],
[name: 'Carol', dept: 'Engineering'],
[name: 'Dave', dept: 'Marketing'],
[name: 'Eve', dept: 'Design']
]
def uniqueDepts = people.unique(false) { it.dept }
println "\nUnique by department:"
uniqueDepts.each { println " ${it.name} - ${it.dept}" }
// toSet() - fast deduplication (order not guaranteed)
def items = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
def itemSet = items.toSet()
println "\ntoSet(): ${itemSet}"
println "Type: ${itemSet.getClass().simpleName}"
Output
Original: [Alice, ALICE, alice, Bob, BOB, Charlie] Case-insensitive unique: [Alice, Bob, Charlie] Unique by first letter: [hello, world] Unique by department: Alice - Engineering Bob - Marketing Eve - Design toSet(): [1, 2, 3, 4, 5, 6, 9] Type: HashSet
What happened here: When you pass a two-argument closure to unique(), it acts as a comparator – returning 0 means “these are duplicates.” When you pass a one-argument closure, Groovy uses it as a key extractor – elements with the same key are considered duplicates. The toSet() method converts the list to a plain HashSet, which naturally eliminates duplicates but makes no promise about iteration order. Our output only looks sorted because small integer hash codes match their values. Sets also have different semantics than lists (no index-based access), so convert back to a list with .toList() if needed.
Example 9: findAll() to Filter and clear()
What we’re doing: Using findAll() as a non-destructive alternative to removal, and clear() to empty a list entirely.
Example 9: findAll() to Filter and clear()
// findAll - returns a NEW list with matching elements
def temperatures = [72, 68, 85, 91, 77, 64, 88, 95, 70]
println "All temps: ${temperatures}"
def comfortable = temperatures.findAll { it >= 70 && it <= 85 }
println "Comfortable (70-85): ${comfortable}"
println "Original unchanged: ${temperatures}"
// findAll is the functional alternative to removeAll
def words = ['Groovy', '', 'is', null, 'awesome', '', '!']
println "\nRaw words: ${words}"
def cleaned = words.findAll { it } // removes nulls and empty strings
println "Cleaned: ${cleaned}"
// Chain findAll with other operations
def scores = [45, 82, 91, 33, 76, 88, 95, 60, 71, 100]
def topScores = scores.findAll { it >= 80 }.sort().reverse()
println "\nTop scores (sorted desc): ${topScores}"
// clear() - remove everything
def data = [1, 2, 3, 4, 5]
println "\nBefore clear: ${data}"
data.clear()
println "After clear: ${data}"
println "Size: ${data.size()}"
println "Is empty: ${data.isEmpty()}"
Output
All temps: [72, 68, 85, 91, 77, 64, 88, 95, 70] Comfortable (70-85): [72, 85, 77, 70] Original unchanged: [72, 68, 85, 91, 77, 64, 88, 95, 70] Raw words: [Groovy, , is, null, awesome, , !] Cleaned: [Groovy, is, awesome, !] Top scores (sorted desc): [100, 95, 91, 88, 82] Before clear: [1, 2, 3, 4, 5] After clear: [] Size: 0 Is empty: true
What happened here: The findAll() method returns a new list containing only elements for which the closure returns true. It is the functional, non-destructive counterpart to removeAll(). When you write findAll { it }, Groovy treats null, empty strings, zero, and false as falsy values, making it a convenient way to strip out junk. The clear() method empties the list completely. It is simple but worth knowing – especially when you want to reuse the same list object rather than assigning a new empty list.
Example 10: Real-World Data Cleaning Pipeline
What we’re doing: Combining contains, remove, and deduplication in a real-world scenario – cleaning imported CSV data.
Example 10: Real-World Data Cleaning
// Simulating raw data from a CSV import
def rawEmails = [
'alice@company.com',
'Bob@Company.COM',
'alice@company.com', // exact duplicate
'', // empty
'charlie@company.com',
null, // null entry
'ALICE@COMPANY.COM', // case duplicate
'invalid-email', // no @ sign
'bob@company.com', // case duplicate
'dave@company.com',
' eve@company.com ', // has whitespace
'dave@company.com' // exact duplicate
]
println "Raw data (${rawEmails.size()} entries):"
rawEmails.each { println " '${it}'" }
// Step 1: Remove nulls and empty strings
def step1 = rawEmails.findAll { it?.trim() }
println "\nAfter removing nulls/empty (${step1.size()}): ${step1}"
// Step 2: Trim whitespace and normalize to lowercase
def step2 = step1.collect { it.trim().toLowerCase() }
println "After trim + lowercase (${step2.size()}): ${step2}"
// Step 3: Remove invalid emails
def step3 = step2.findAll { it.contains('@') && it.contains('.') }
println "After validation (${step3.size()}): ${step3}"
// Step 4: Deduplicate
def step4 = step3.unique(false)
println "After dedup (${step4.size()}): ${step4}"
// Step 5: Remove blocked domains
def blockedDomains = ['blocked.com', 'spam.org']
def step5 = step4.findAll { email ->
!blockedDomains.any { domain -> email.endsWith("@${domain}") }
}
println "After domain filter (${step5.size()}): ${step5}"
println "\nFinal clean list:"
step5.eachWithIndex { email, i -> println " ${i + 1}. ${email}" }
Output
Raw data (12 entries): 'alice@company.com' 'Bob@Company.COM' 'alice@company.com' '' 'charlie@company.com' 'null' 'ALICE@COMPANY.COM' 'invalid-email' 'bob@company.com' 'dave@company.com' ' eve@company.com ' 'dave@company.com' After removing nulls/empty (10): [alice@company.com, Bob@Company.COM, alice@company.com, charlie@company.com, ALICE@COMPANY.COM, invalid-email, bob@company.com, dave@company.com, eve@company.com , dave@company.com] After trim + lowercase (10): [alice@company.com, bob@company.com, alice@company.com, charlie@company.com, alice@company.com, invalid-email, bob@company.com, dave@company.com, eve@company.com, dave@company.com] After validation (9): [alice@company.com, bob@company.com, alice@company.com, charlie@company.com, alice@company.com, bob@company.com, dave@company.com, eve@company.com, dave@company.com] After dedup (5): [alice@company.com, bob@company.com, charlie@company.com, dave@company.com, eve@company.com] After domain filter (5): [alice@company.com, bob@company.com, charlie@company.com, dave@company.com, eve@company.com] Final clean list: 1. alice@company.com 2. bob@company.com 3. charlie@company.com 4. dave@company.com 5. eve@company.com
What happened here: This example brings together everything we covered. We used findAll() to strip nulls and empties, collect() to normalize, findAll() again to validate, unique(false) to deduplicate, and any() inside a filter to check against blocked domains. This pipeline pattern – where you chain non-destructive list operations – is idiomatic Groovy and keeps your data transformations clean and testable.
Removing Elements from Lists
Let us summarize the removal methods and when to use each one. Choosing the right method depends on whether you want to modify the original list or create a new one, and whether you are removing by position, value, or condition.
Destructive vs Non-Destructive Removal
| Approach | Method | Modifies Original? | Best For |
|---|---|---|---|
| Remove by index | remove(int) | Yes | Known position |
| Remove first match | removeElement(obj) | Yes | Single value removal |
| Remove all matches | removeAll{} | Yes | Conditional bulk removal |
| Remove if predicate | removeIf{} | Yes | Java 8 style predicate |
| Keep only matches | retainAll{} | Yes | Whitelist filtering |
| Empty the list | clear() | Yes | Reset to empty |
| Filter to new list | findAll{} | No | Functional filtering |
| Subtract elements | list - elements | No | Set-like subtraction |
As a general rule, prefer non-destructive methods (findAll(), minus operator) in functional-style code. Use destructive methods (removeAll(), retainAll()) when performance matters and you want to avoid creating new list objects.
Deduplication Techniques
Groovy gives you multiple ways to deduplicate. Here is a quick comparison to help you choose:
Deduplication Techniques Compared
def data = [5, 3, 1, 3, 5, 7, 1, 9, 3]
println "Original: ${data}"
// Method 1: unique() - modifies in place
def m1 = data.clone() as List
m1.unique()
println "unique() in-place: ${m1}"
// Method 2: unique(false) - returns new list
def m2 = data.unique(false)
println "unique(false): ${m2}"
// Method 3: toSet().toList() - via Set
def m3 = data.toSet().toList()
println "toSet().toList(): ${m3}"
// Method 4: toUnique() - always returns new list (Groovy 2.4+)
def m4 = data.toUnique()
println "toUnique(): ${m4}"
// Method 5: minus operator for specific removals
def m5 = data - [3] // remove all 3s (not exactly dedup, but useful)
println "data - [3]: ${m5}"
// Method 6: unique with closure for custom dedup
def words = ['Hello', 'HELLO', 'World', 'world', 'hello']
def m6 = words.unique(false) { it.toLowerCase() }
println "\nCase-insensitive dedup: ${m6}"
println "\nOriginal data unchanged: ${data}"
Output
Original: [5, 3, 1, 3, 5, 7, 1, 9, 3] unique() in-place: [5, 3, 1, 7, 9] unique(false): [5, 3, 1, 7, 9] toSet().toList(): [1, 3, 5, 7, 9] toUnique(): [5, 3, 1, 7, 9] data - [3]: [5, 1, 5, 7, 1, 9] Case-insensitive dedup: [Hello, World] Original data unchanged: [5, 3, 1, 3, 5, 7, 1, 9, 3]
Notice the difference: unique() and toUnique() preserve insertion order, while toSet().toList() may reorder elements because toSet() returns a plain HashSet with no order guarantee. For most use cases, unique(false) or toUnique() is the safest choice.
Real-World: Cleaning Data
Let us look at a few more real-world scenarios where contains, remove, and deduplication work together in production code.
Scenario: Tag Management System
Tag Management System
// A simple tag management system
class TagManager {
List<String> tags = []
void addTag(String tag) {
def normalized = tag.trim().toLowerCase()
if (!(normalized in tags) && normalized) {
tags << normalized
}
}
void addTags(List<String> newTags) {
newTags.each { addTag(it) }
}
void removeTag(String tag) {
tags.removeElement(tag.trim().toLowerCase())
}
boolean hasTag(String tag) {
return tag.trim().toLowerCase() in tags
}
boolean hasAnyOf(List<String> checkTags) {
return checkTags.any { hasTag(it) }
}
boolean hasAllOf(List<String> checkTags) {
return checkTags.every { hasTag(it) }
}
}
def tm = new TagManager()
tm.addTags(['Groovy', 'Java', 'KOTLIN', ' groovy ', 'java', 'Scala'])
println "Tags: ${tm.tags}"
println "Has 'groovy': ${tm.hasTag('Groovy')}"
println "Has any JVM: ${tm.hasAnyOf(['java', 'kotlin', 'clojure'])}"
println "Has all JVM: ${tm.hasAllOf(['java', 'kotlin', 'clojure'])}"
tm.removeTag('Scala')
println "After removing Scala: ${tm.tags}"
Output
Tags: [groovy, java, kotlin, scala] Has 'groovy': true Has any JVM: true Has all JVM: false After removing Scala: [groovy, java, kotlin]
Scenario: Log Entry Deduplication
Log Entry Deduplication
// Deduplicate log entries by message content (ignore timestamps)
def logEntries = [
[time: '10:01', level: 'ERROR', msg: 'Connection timeout'],
[time: '10:02', level: 'WARN', msg: 'Slow query detected'],
[time: '10:03', level: 'ERROR', msg: 'Connection timeout'],
[time: '10:04', level: 'INFO', msg: 'User login successful'],
[time: '10:05', level: 'ERROR', msg: 'Connection timeout'],
[time: '10:06', level: 'WARN', msg: 'Slow query detected'],
[time: '10:07', level: 'INFO', msg: 'Cache refreshed']
]
println "Total log entries: ${logEntries.size()}"
// Deduplicate by message, keeping first occurrence
def uniqueLogs = logEntries.unique(false) { it.msg }
println "Unique by message: ${uniqueLogs.size()}"
uniqueLogs.each { println " [${it.time}] ${it.level}: ${it.msg}" }
// Count occurrences of each message
def counts = logEntries.countBy { it.msg }
println "\nMessage frequency:"
counts.each { msg, count -> println " ${msg}: ${count} times" }
// Find repeated errors (possible issue)
def repeatedErrors = counts.findAll { msg, count ->
count > 1 && logEntries.find { it.msg == msg }?.level == 'ERROR'
}
println "\nRepeated errors to investigate:"
repeatedErrors.each { msg, count -> println " ${msg} (${count}x)" }
Output
Total log entries: 7 Unique by message: 4 [10:01] ERROR: Connection timeout [10:02] WARN: Slow query detected [10:04] INFO: User login successful [10:07] INFO: Cache refreshed Message frequency: Connection timeout: 3 times Slow query detected: 2 times User login successful: 1 times Cache refreshed: 1 times Repeated errors to investigate: Connection timeout (3x)
These real-world examples show how Groovy’s list operations compose naturally. The tag manager uses in, removeElement(), any(), and every() together. The log deduplicator uses unique() with a closure, countBy(), and findAll() in a pipeline. This composability is one of Groovy’s strongest features.
Edge Cases and Best Practices
Best Practices Summary
DO:
- Use
removeElement()instead ofremove()when removing integer values from integer lists - Use
unique(false)ortoUnique()when you want to keep the original list intact - Use
findAll()for functional-style filtering that returns a new list - Use the
inoperator for readable containment checks in if statements - Normalize data (trim, lowercase) before deduplication for accurate results
DON’T:
- Call
remove(2)on an integer list expecting it to remove the value 2 – it removes at index 2 - Use
unique()without arguments if you need the original list unchanged – it mutates in place - Iterate over a list and call
remove()during iteration – useremoveAll{}orremoveIf{}instead - Forget that
contains()usesequals()– so object identity and equality matter
Edge Case: Removing During Iteration
Safe vs Unsafe Removal During Iteration
def items = [1, 2, 3, 4, 5, 6, 7, 8]
// WRONG: ConcurrentModificationException risk
// items.each { if (it % 2 == 0) items.remove(it) } // Don't do this!
// RIGHT: Use removeAll with closure
items.removeAll { it % 2 == 0 }
println "After removeAll even: ${items}"
// RIGHT: Use removeIf
def items2 = [1, 2, 3, 4, 5, 6, 7, 8]
items2.removeIf { it > 5 }
println "After removeIf > 5: ${items2}"
// RIGHT: Use findAll to create new list
def items3 = [1, 2, 3, 4, 5, 6, 7, 8]
def odds = items3.findAll { it % 2 != 0 }
println "Odds via findAll: ${odds}"
Output
After removeAll even: [1, 3, 5, 7] After removeIf > 5: [1, 2, 3, 4, 5] Odds via findAll: [1, 3, 5, 7]
Performance Considerations
For most day-to-day list operations, performance is not a concern. But when a Groovy list contains check runs inside a loop over thousands or millions of elements, the choice of method matters:
- contains() on ArrayList: O(n) linear scan. For frequent lookups on large datasets, convert to a
Setfirst –toSet()gives O(1) lookup - removeAll() with closure: O(n) single pass. Much better than calling
remove()in a loop, which is O(n) for each removal due to array shifting - unique(): O(n) time and space for the default implementation. Using a closure-based comparator can increase this to O(n log n) due to sorting
- findAll(): O(n) and creates a new list. If memory is tight and you are working with very large lists,
removeAll()in-place avoids the extra allocation - Minus operator (-): Creates a new list each time. Chaining multiple minus operations is expensive – batch them into a single
removeAll()call
Performance: List vs Set for Lookups
def largeList = (1..10000).toList()
def lookupSet = largeList.toSet()
// Slow: O(n) per lookup
def start1 = System.nanoTime()
1000.times { largeList.contains(9999) }
def time1 = (System.nanoTime() - start1) / 1_000_000
// Fast: O(1) per lookup
def start2 = System.nanoTime()
1000.times { lookupSet.contains(9999) }
def time2 = (System.nanoTime() - start2) / 1_000_000
println "List contains (1000 lookups): ${time1}ms"
println "Set contains (1000 lookups): ${time2}ms"
println "Set is ~${(time1 / time2).intValue()}x faster for lookups"
Output
List contains (1000 lookups): 79.2718ms Set contains (1000 lookups): 12.622ms Set is ~6x faster for lookups
The takeaway: if you are calling contains() repeatedly, convert to a Set first. The one-time cost of toSet() pays for itself after just a few lookups on large lists. Exact millisecond numbers will differ on your machine and even between runs (JVM warmup alone shifts them), but the Set stays several times faster every time.
Conclusion
We covered the full spectrum of Groovy list contains, remove, and deduplicate operations in this post. From the basic contains() and in operator to the powerful find(), any(), and every() methods, Groovy gives you far more tools for searching lists than Java alone.
For removal, you have remove(), removeElement(), removeAll(), removeIf(), retainAll(), the minus operator, findAll(), and clear(). And for deduplication, unique() with its closure support is genuinely elegant.
The biggest pitfall to remember: remove(2) on an integer list removes the element at index 2, not the value 2. Always use removeElement() when you mean to remove by value.
If you are just getting started with Groovy lists, check out our Groovy List Tutorial for the basics, and keep the Set conversion trick in your pocket for the day a Groovy list contains check shows up in a profiler. For array operations and more advanced manipulation, head to our next post on Groovy Array Manipulation.
Summary
contains()and theinoperator check for exact element matchesfind(),any(), andevery()let you search with closures for flexible matching- Use
removeElement()for value-based removal to avoid the index/value ambiguity unique(false)andfindAll()are safe – they return new lists without modifying the original- For frequent
contains()checks on large data, convert to a Set for O(1) lookups
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 Array Manipulation Examples
Frequently Asked Questions
How do I check if a Groovy list contains an element?
Use the contains() method or the in operator. For example: ['a','b','c'].contains('b') returns true, and ‘b’ in ['a','b','c'] also returns true. For conditional checks, use find{} or any{} with a closure. The in operator is preferred for readability in if statements.
What is the difference between remove() and removeElement() in Groovy?
The remove(int) method removes the element at a specific index position. The removeElement(Object) method removes the first occurrence of a specific value. This distinction is critical for integer lists: remove(2) removes the element at index 2, while removeElement(2) removes the first occurrence of the value 2. Always use removeElement() when removing by value from integer lists.
How do I remove duplicates from a Groovy list?
Use unique() to remove duplicates in place, or unique(false) to get a new deduplicated list without modifying the original. You can also use toUnique(), toSet().toList(), or unique(false) { closure } for custom deduplication logic. For case-insensitive deduplication, pass a closure: list.unique(false) { it.toLowerCase() }.
Does Groovy’s unique() method modify the original list?
Yes, by default unique() modifies the list in place and returns a reference to the same list. To get a new list without modifying the original, pass false as the first argument: list.unique(false). Alternatively, use toUnique() which always returns a new list. This is a common gotcha that catches many developers.
How do I remove all elements matching a condition from a Groovy list?
Use removeAll { condition } or removeIf { condition } to remove elements in place. For example: list.removeAll { it > 10 } removes all elements greater than 10. If you prefer a non-destructive approach, use findAll { condition } to create a new list with only the elements you want to keep: list.findAll { it <= 10 }.
Interview Questions on Groovy List Contains and Remove
Try each one aloud first; the phrasing you produce under mild pressure is what interviews measure.
Q: A Jenkins job filters 200,000 incoming records with if (record.id in seenIds) where seenIds is a List, and the stage now takes 40 minutes. What’s your diagnosis and fix?
For a List, the in operator delegates to contains(), which is a linear scan, so 200,000 checks against a growing list is quadratic, billions of comparisons. The fix is one line: build seenIds as a HashSet, where contains() is a constant-time hash lookup, and the stage drops to seconds. The caveat worth adding: hashing only works if the element type has sane hashCode() and equals(), and if you also need insertion order, LinkedHashSet gives you both. Recognizing “slow membership test on a List” as a data structure smell is the whole point of the question.
Q: A list of Employee objects clearly holds the record you’re looking for, you can see it in the debugger, yet employees.contains(target) returns false. What’s missing?
contains() compares with equals(), and a class that never overrides it inherits identity comparison from Object, so two Employee instances with identical fields are “different” unless they’re literally the same reference. The debugger shows matching data; equals() doesn’t look at data. In Groovy the clean fix is the @EqualsAndHashCode AST transformation on the class, which generates both methods from the fields, and both matter: hashCode() must agree with equals() or Sets and Maps misbehave next. If you can’t touch the class, search by predicate instead: employees.any { it.id == target.id }.
Q: A payment reconciliation script checks amounts.contains(1.5G) and misses records that show as 1.50 in the database export. Both are BigDecimals. Why the miss?
BigDecimal.equals() compares value and scale, so 1.5G and 1.50G are not equal even though compareTo() says they are numerically identical. contains() uses equals(), so the scale difference from the export makes every lookup miss. Fixes: normalize scale on the way in with setScale() or stripTrailingZeros(), or search with a comparison-based predicate like amounts.any { it.compareTo(target) == 0 }. This is a classic in finance shops, and naming the equals-versus-compareTo split for BigDecimal is exactly the signal the interviewer is fishing for.
Q: A colleague writes blocked - expired to purge expired entries from a blocklist and commits it. The list never shrinks in production. No errors anywhere. What happened?
The minus operator is non-mutating: it builds and returns a new list, and since the result was never assigned, it was constructed and thrown away. The original list is untouched, which is why nothing errors and nothing changes. The fix is either assigning the result, blocked = blocked - expired or the shorthand blocked -= expired, or using a mutating call like removeAll(). The interview follow-up is usually “which style do you prefer,” and the defensible answer is the non-mutating one for shared references, mutating for tight loops where allocation matters.
Q: To keep only whitelisted users, someone calls users.removeAll(whitelist) and ships it. What did they actually deploy, and how would you catch this class of bug earlier?
They deployed the exact opposite of the requirement: removeAll() deletes every element that IS in the whitelist, keeping the unlisted ones. The intended call was retainAll(whitelist), which keeps the intersection and removes everything else. The two methods are mirror images and both return quietly, so nothing fails at runtime. Catching it early is about tests that assert on outcomes, a one-liner asserting a non-whitelisted user is gone would have failed immediately, and about naming intent in code review: “remove everything except” should always trigger a retainAll() double-check.
Related Posts
Previous in Series: Groovy List to String Conversion
Next in Series: Groovy Array Manipulation Examples
Related Topics You Might Like:
- Groovy List Tutorial – The Complete Guide
- Groovy Array Manipulation Examples
- Groovy String Tutorial – The Complete Guide
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment