10 Groovy List Contains and Remove Methods Explained

Groovy list contains checks are easy -, remove, and deduplicate operations simple with 10 tested examples. Find, remove, and clean list data on Groovy 5.x.

“The art of programming is the art of organizing complexity – and lists that contain duplicates and stale entries are complexity waiting to bite you.”

Robert C. Martin, Clean Code

Last Updated: March 2026 | Tested on: Groovy 5.x, 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.x with verified output. Together, they give you the complete picture of how to search, clean, and deduplicate lists in Groovy.

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 element
  • containsAll(collection) – returns true if the list has all elements from another collection
  • element in list – the in operator, syntactic sugar for contains()
  • find { condition } – returns the first element matching a closure, or null
  • any { condition } – returns true if at least one element matches
  • every { condition } – returns true if all elements match

Syntax and Method Overview

MethodPurposeReturnsModifies List?
contains()Check for exact elementbooleanNo
containsAll()Check for multiple elementsbooleanNo
in operatorReadable containment checkbooleanNo
find{}First matching elementelement or nullNo
any{}At least one matches?booleanNo
every{}All match?booleanNo
remove(index)Remove by positionremoved elementYes
removeElement(value)Remove first occurrencebooleanYes
removeAll{}Remove all matchingbooleanYes
removeIf{}Remove if predicate truebooleanYes
retainAll{}Keep only matchingbooleanYes
unique()Remove duplicates in-placethe listYes
unique(false)Deduplicate without modifyingnew listNo
toSet()Convert to Set (no dupes)SetNo
minus operator (-)Subtract elementsnew listNo

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: LinkedHashSet

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 LinkedHashSet, which naturally eliminates duplicates. However, sets have different semantics than lists (no index-based access, no guaranteed iteration order in some Set implementations), 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

ApproachMethodModifies Original?Best For
Remove by indexremove(int)YesKnown position
Remove first matchremoveElement(obj)YesSingle value removal
Remove all matchesremoveAll{}YesConditional bulk removal
Remove if predicateremoveIf{}YesJava 8 style predicate
Keep only matchesretainAll{}YesWhitelist filtering
Empty the listclear()YesReset to empty
Filter to new listfindAll{}NoFunctional filtering
Subtract elementslist - elementsNoSet-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 (though LinkedHashSet usually preserves order in practice). 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 of remove() when removing integer values from integer lists
  • Use unique(false) or toUnique() when you want to keep the original list intact
  • Use findAll() for functional-style filtering that returns a new list
  • Use the in operator 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 – use removeAll{} or removeIf{} instead
  • Forget that contains() uses equals() – 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 you are processing 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 Set first – 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): 38ms
Set contains  (1000 lookups): 1ms
Set is ~38x 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.

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. For array operations and more advanced manipulation, head to our next post on Groovy Array Manipulation.

Summary

  • contains() and the in operator check for exact element matches
  • find(), any(), and every() let you search with closures for flexible matching
  • Use removeElement() for value-based removal to avoid the index/value ambiguity
  • unique(false) and findAll() 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 }.

Previous in Series: Groovy List to String Conversion

Next in Series: Groovy Array Manipulation Examples

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 *