Learn how to use Groovy find() method with 10 practical examples. Find first matching element in lists, maps, and strings. Tested on Groovy 5.x.
“Finding a needle in a haystack is easy when you have the right method. In Groovy, that method is find().”
Kent Beck, Test-Driven Development
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 16 minutes
Every developer hits this scenario: you have a list of items and you need the first one that matches some condition – the first even number, the first admin user, the first config entry with a specific key. In Java, you’d write a loop, check each element, and break when you find it. With groovy find via the find() method, it’s a single line.
The Groovy find() method is one of the most useful methods in the GDK (Groovy Development Kit). It iterates through a collection, applies your closure condition, and returns the first element that matches. No loops, no index tracking, no break statements. Just a clean, readable one-liner.
In this post, we’ll work through 10 practical examples of find() on lists, maps, strings, and custom objects. We’ll also compare it to related methods like findAll(), any(), findResult(), and findIndexOf(). If you’re coming from the each() loop tutorial, think of find() as a smarter version of each() that stops at the first match and returns it.
Table of Contents
What Is the Groovy find() Method?
The find() method is a GDK method added to all Groovy collections (and several other types like arrays and strings). It accepts a closure as a predicate and returns the first element for which the closure returns a truthy value. If no element matches, it returns null.
According to the official Groovy GDK documentation, find() is defined on Iterable, Object[], Map, and CharSequence. That means you can use it on practically anything.
Key Points:
- Returns the first matching element, not all matches
- Returns
nullwhen no match is found - Stops iterating as soon as a match is found (short-circuit evaluation)
- Works on Lists, Sets, Maps, Arrays, Strings, and any Iterable
- Uses Groovy’s truth rules – the closure must return a truthy value
- For finding all matches, use findAll() instead
Syntax and Basic Usage
The basic syntax is simple:
find() Syntax
// On a collection
collection.find { element -> condition }
// On a map (closure receives a Map.Entry)
map.find { entry -> condition }
// or with key, value
map.find { key, value -> condition }
// Without a closure - finds first truthy element
collection.find()
Here it is in action with a quick example before the full set:
Basic find() Usage
def numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// Find the first even number
def firstEven = numbers.find { it % 2 == 0 }
println "First even: ${firstEven}"
// Find the first number greater than 7
def firstBig = numbers.find { it > 7 }
println "First > 7: ${firstBig}"
// Find something that doesn't exist
def notFound = numbers.find { it > 100 }
println "Not found: ${notFound}"
Output
First even: 2 First > 7: 8 Not found: null
Simple, right? The closure { it % 2 == 0 } is the condition. Groovy iterates through the list, and the moment it finds an element where the closure returns true, it stops and returns that element. If nothing matches, you get null.
10 Practical find() Examples
Example 1: find() on a List of Numbers
What we’re doing: Finding the first number that meets various conditions in a numeric list.
Example 1: find() on Numbers
def numbers = [3, 7, 12, 5, 18, 2, 25, 9, 14]
// First number divisible by 3
def divBy3 = numbers.find { it % 3 == 0 }
println "First divisible by 3: ${divBy3}"
// First number greater than 10
def greaterThan10 = numbers.find { it > 10 }
println "First > 10: ${greaterThan10}"
// First negative number (none exist)
def negative = numbers.find { it < 0 }
println "First negative: ${negative}"
// First perfect square
def perfectSquare = numbers.find { Math.sqrt(it) == Math.sqrt(it).intValue() }
println "First perfect square: ${perfectSquare}"
// find() without a closure - returns first truthy element
def mixed = [0, '', null, false, 42, 'hello']
def firstTruthy = mixed.find()
println "First truthy: ${firstTruthy}"
Output
First divisible by 3: 3 First > 10: 12 First negative: null First perfect square: 25 First truthy: 42
What happened here: Each find() call scans the list from left to right and returns the first element where the closure evaluates to true. When called without a closure, find() returns the first element that is truthy in Groovy’s truth rules (non-null, non-zero, non-empty, non-false).
Example 2: find() on a List of Strings
What we’re doing: Searching through a list of strings to find the first match based on different criteria.
Example 2: find() on Strings
def languages = ['Java', 'Groovy', 'Kotlin', 'Scala', 'Clojure', 'Grails']
// First language starting with 'G'
def startsWithG = languages.find { it.startsWith('G') }
println "First starting with G: ${startsWithG}"
// First language with more than 5 characters
def longName = languages.find { it.length() > 5 }
println "First with > 5 chars: ${longName}"
// First language containing 'oo'
def containsOo = languages.find { it.contains('oo') }
println "First containing 'oo': ${containsOo}"
// Case-insensitive search
def fruits = ['Apple', 'BANANA', 'cherry', 'Date']
def found = fruits.find { it.toLowerCase() == 'banana' }
println "Found banana: ${found}"
// Find using regex
def codes = ['AB-100', 'CD-200', 'EF-300', 'GH-400']
def match = codes.find { it ==~ /[CE].+-\d+/ }
println "Regex match: ${match}"
Output
First starting with G: Groovy First with > 5 chars: Groovy First containing 'oo': Groovy Found banana: BANANA Regex match: CD-200
What happened here: The find() method works beautifully with string operations. Notice that the original element is returned as-is – when we searched for “banana” case-insensitively, we got back “BANANA” (the original casing). The regex example uses Groovy’s match operator ==~ inside the closure.
Example 3: find() on a Map
What we’re doing: Using find() on a map to locate entries by key, value, or both.
Example 3: find() on Maps
def scores = [Alice: 85, Bob: 92, Charlie: 78, Diana: 95, Eve: 88]
// Find first entry where score > 90
def highScorer = scores.find { key, value -> value > 90 }
println "First high scorer: ${highScorer}"
println " Key: ${highScorer.key}, Value: ${highScorer.value}"
// Find using single parameter (Map.Entry)
def entry = scores.find { it.value == 78 }
println "Score 78: ${entry}"
// Find by key pattern
def config = [
'db.host' : 'localhost',
'db.port' : '5432',
'app.name' : 'MyApp',
'db.password' : 'secret',
'app.version' : '2.1'
]
def firstDbConfig = config.find { k, v -> k.startsWith('db.') }
println "First db config: ${firstDbConfig.key} = ${firstDbConfig.value}"
// Find entry where value is numeric
def numericEntry = config.find { k, v -> v.isNumber() }
println "First numeric value: ${numericEntry.key} = ${numericEntry.value}"
Output
First high scorer: Bob=92 Key: Bob, Value: 92 Score 78: Charlie=78 First db config: db.host = localhost First numeric value: db.port = 5432
What happened here: When you call find() on a Map, the closure receives either a single Map.Entry parameter or two parameters (key and value). The return type is a Map.Entry object, so you can access both .key and .value on the result.
Example 4: find() on Strings (CharSequence)
What we’re doing: Using find() to search within strings for characters and patterns.
Example 4: find() on Strings
def text = "Hello, Groovy 5.0 is awesome!"
// find() with a regex pattern - returns matching substring
def firstNumber = text.find(/\d+\.?\d*/)
println "First number: ${firstNumber}"
// Find first uppercase letter
def firstUpper = text.find(/[A-Z]/)
println "First uppercase: ${firstUpper}"
// Find first word with 5+ characters
def longWord = text.find(/\b\w{5,}\b/)
println "First long word: ${longWord}"
// find() on string as a collection of characters
def firstVowel = text.toList().find { it.toLowerCase() in ['a', 'e', 'i', 'o', 'u'] }
println "First vowel: ${firstVowel}"
// Find a specific pattern
def email = "Contact us at support@techno.com or admin@techno.com"
def firstEmail = email.find(/[\w.]+@[\w.]+\.\w+/)
println "First email: ${firstEmail}"
Output
First number: 5.0 First uppercase: H First long word: Hello First vowel: e First email: support@techno.com
What happened here: There are two flavors of find() on strings. When you pass a regex pattern (like text.find(/\d+/)), it returns the first substring that matches the pattern. When you convert the string to a list of characters with toList() and then call find(), you’re searching character by character. Both are incredibly useful.
Example 5: find() with Complex Conditions
What we’re doing: Combining multiple conditions inside the find() closure for more sophisticated matching.
Example 5: Complex Conditions
def products = [
[name: 'Laptop', price: 999, inStock: true, category: 'Electronics'],
[name: 'Book', price: 15, inStock: false, category: 'Education'],
[name: 'Phone', price: 699, inStock: true, category: 'Electronics'],
[name: 'Desk', price: 250, inStock: true, category: 'Furniture'],
[name: 'Tablet', price: 449, inStock: true, category: 'Electronics'],
[name: 'Pen', price: 3, inStock: true, category: 'Stationery']
]
// Find first affordable electronics item in stock
def affordable = products.find { p ->
p.category == 'Electronics' &&
p.inStock &&
p.price < 500
}
println "Affordable electronics: ${affordable.name} (\$${affordable.price})"
// Find first product under $20 that's in stock
def cheap = products.find { it.inStock && it.price < 20 }
println "Cheap & in stock: ${cheap.name}"
// Multiple conditions with method calls
def names = ['bob', 'ALICE', ' charlie ', 'Dave', 'eve']
def cleanName = names.find { it.trim().length() > 3 && it == it.toLowerCase() }
println "First clean lowercase name > 3 chars: ${cleanName}"
Output
Affordable electronics: Tablet ($449) Cheap & in stock: Pen First clean lowercase name > 3 chars: charlie
What happened here: The closure in find() can contain any valid Groovy expression. You can use &&, ||, method calls, nested conditions – whatever you need. The closure just needs to return something truthy for a match. Notice that the original element is returned as-is, including any whitespace (like ” charlie “).
Example 6: find() on Custom Objects
What we’re doing: Using find() with a list of custom Groovy objects (classes and @Canonical).
Example 6: Custom Objects
import groovy.transform.Canonical
@Canonical
class Employee {
String name
String department
double salary
boolean active
}
def employees = [
new Employee('Alice', 'Engineering', 95000, true),
new Employee('Bob', 'Marketing', 72000, false),
new Employee('Charlie', 'Engineering', 88000, true),
new Employee('Diana', 'HR', 68000, true),
new Employee('Eve', 'Engineering', 105000, true)
]
// Find first active engineer
def engineer = employees.find { it.department == 'Engineering' && it.active }
println "First active engineer: ${engineer.name} (\$${engineer.salary})"
// Find first employee earning over 100k
def highEarner = employees.find { it.salary > 100000 }
println "First high earner: ${highEarner.name}"
// Find first inactive employee
def inactive = employees.find { !it.active }
println "First inactive: ${inactive.name}, ${inactive.department}"
// Find by name pattern
def nameMatch = employees.find { it.name ==~ /[A-C].*/ }
println "First name A-C: ${nameMatch.name}"
Output
First active engineer: Alice ($95000.0) First high earner: Eve First inactive: Bob, Marketing First name A-C: Alice
What happened here: The find() method works well with custom objects. You access properties just like you would in any Groovy closure. The @Canonical annotation gives us toString(), equals(), and hashCode() for free, making it easy to work with these objects.
Example 7: Chaining find() with Other Methods
What we’re doing: Combining find() with other GDK methods like collect(), sort(), and the safe navigation operator.
Example 7: Chaining Methods
def numbers = [45, 12, 89, 3, 67, 23, 91, 56]
// Sort first, then find - gets the smallest number > 50
def smallestOver50 = numbers.sort(false).find { it > 50 }
println "Smallest > 50 (sorted): ${smallestOver50}"
// Collect then find - transform, then search
def words = ['hello', 'world', 'groovy', 'code']
def firstLong = words.collect { it.toUpperCase() }.find { it.length() > 4 }
println "First uppercase word > 4 chars: ${firstLong}"
// find() + safe navigation (?.) for null-safe property access
def users = [
[name: 'Alice', email: 'alice@test.com'],
[name: 'Bob', email: null],
[name: 'Charlie', email: 'charlie@test.com']
]
def firstEmailUser = users.find { it.email != null }
println "First user with email: ${firstEmailUser.name} (${firstEmailUser.email})"
// Chaining find result with safe navigation
def result = users.find { it.name == 'Unknown' }?.email ?: 'No email found'
println "Unknown user email: ${result}"
// unique() + find
def dupes = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
def firstUniqueOver4 = dupes.unique(false).find { it > 4 }
println "First unique > 4: ${firstUniqueOver4}"
Output
Smallest > 50 (sorted): 56 First uppercase word > 4 chars: HELLO First user with email: Alice (alice@test.com) Unknown user email: No email found First unique > 4: 5
What happened here: The find() method chains beautifully with other GDK methods. Notice two important patterns: (1) Using sort(false) and unique(false) to avoid modifying the original list. (2) Using the safe navigation operator ?. after find() to safely handle null results – since find() returns null when nothing matches, chaining without ?. would throw a NullPointerException.
Example 8: Real-World – Searching Records
What we’re doing: Simulating real-world scenarios like searching user records, log entries, and database results.
Example 8: Searching Records
// Simulated user database
def users = [
[id: 1, username: 'admin', role: 'ADMIN', lastLogin: '2026-03-01'],
[id: 2, username: 'jdoe', role: 'USER', lastLogin: '2026-02-15'],
[id: 3, username: 'manager', role: 'MANAGER', lastLogin: '2026-03-07'],
[id: 4, username: 'guest', role: 'USER', lastLogin: null],
[id: 5, username: 'support', role: 'ADMIN', lastLogin: '2026-03-05']
]
// Find user by username
def admin = users.find { it.username == 'admin' }
println "Admin user: id=${admin.id}, lastLogin=${admin.lastLogin}"
// Find first user who never logged in
def neverLoggedIn = users.find { it.lastLogin == null }
println "Never logged in: ${neverLoggedIn.username}"
// Simulated log entries
def logs = [
'[INFO] 2026-03-07 10:00 - Application started',
'[WARN] 2026-03-07 10:05 - High memory usage detected',
'[ERROR] 2026-03-07 10:10 - Database connection failed',
'[INFO] 2026-03-07 10:11 - Retrying connection...',
'[ERROR] 2026-03-07 10:12 - Retry failed after 3 attempts'
]
// Find first error
def firstError = logs.find { it.startsWith('[ERROR]') }
println "First error: ${firstError}"
// Find first log after 10:10
def afterTime = logs.find { it.contains('10:1') && !it.contains('10:10') }
println "First log after 10:10: ${afterTime}"
Output
Admin user: id=1, lastLogin=2026-03-01 Never logged in: guest First error: [ERROR] 2026-03-07 10:10 - Database connection failed First log after 10:10: [INFO] 2026-03-07 10:11 - Retrying connection...
What happened here: These examples show how find() shines in real-world use cases. Instead of writing for loops with if checks and break statements, you express the search criteria as a single closure. This pattern is especially common in Grails applications where you might search through domain object lists.
Example 9: Real-World – Finding Config Values
What we’re doing: Using find() to search through configuration data, environment variables, and property files.
Example 9: Config Values
// Application config as a list of maps
def configs = [
[env: 'dev', dbUrl: 'jdbc:h2:mem:devdb', poolSize: 5],
[env: 'staging', dbUrl: 'jdbc:mysql://stage:3306', poolSize: 10],
[env: 'prod', dbUrl: 'jdbc:mysql://prod:3306', poolSize: 50]
]
// Find config for current environment
def currentEnv = 'staging'
def config = configs.find { it.env == currentEnv }
println "Config for ${currentEnv}:"
println " DB URL: ${config.dbUrl}"
println " Pool Size: ${config.poolSize}"
// Feature flags - find first enabled feature
def features = [
[name: 'dark-mode', enabled: false, rollout: 0],
[name: 'new-dashboard', enabled: true, rollout: 50],
[name: 'beta-search', enabled: true, rollout: 25],
[name: 'ai-assistant', enabled: false, rollout: 0]
]
def firstEnabled = features.find { it.enabled }
println "\nFirst enabled feature: ${firstEnabled.name} (${firstEnabled.rollout}% rollout)"
// Find config from prioritized sources (first wins)
def envVars = [DB_HOST: null, DB_PORT: null, APP_NAME: 'FromEnv']
def fileConfig = [DB_HOST: 'localhost', DB_PORT: '5432', APP_NAME: 'FromFile']
def defaults = [DB_HOST: '127.0.0.1', DB_PORT: '3306', APP_NAME: 'DefaultApp']
def sources = [envVars, fileConfig, defaults]
def dbHost = sources.find { it.DB_HOST != null }?.DB_HOST
def appName = sources.find { it.APP_NAME != null }?.APP_NAME
println "\nResolved DB_HOST: ${dbHost}"
println "Resolved APP_NAME: ${appName}"
Output
Config for staging: DB URL: jdbc:mysql://stage:3306 Pool Size: 10 First enabled feature: new-dashboard (50% rollout) Resolved DB_HOST: localhost Resolved APP_NAME: FromEnv
What happened here: Configuration lookups are a natural fit for find(). The last example is a pattern you’ll see a lot in real applications – searching through a prioritized list of configuration sources and taking the first non-null value. The ?. operator ensures we don’t blow up if no source has the key.
Example 10: Real-World – Validating Data
What we’re doing: Using find() to detect the first invalid item in a dataset, which is useful for validation workflows.
Example 10: Data Validation
// Form submissions to validate
def submissions = [
[name: 'Alice', email: 'alice@test.com', age: 28],
[name: '', email: 'bob@test.com', age: 35],
[name: 'Charlie', email: 'invalid-email', age: 22],
[name: 'Diana', email: 'diana@test.com', age: -5]
]
// Find first submission with empty name
def emptyName = submissions.find { it.name.trim().isEmpty() }
println "First empty name: index ${submissions.indexOf(emptyName)}, email: ${emptyName.email}"
// Find first invalid email
def badEmail = submissions.find { !(it.email ==~ /[\w.]+@[\w.]+\.\w+/) }
println "First bad email: ${badEmail.name} -> ${badEmail.email}"
// Find first invalid age
def badAge = submissions.find { it.age < 0 || it.age > 150 }
println "First bad age: ${badAge.name} -> ${badAge.age}"
// Combined validation - find first record with ANY error
def firstInvalid = submissions.find { record ->
record.name.trim().isEmpty() ||
!(record.email ==~ /[\w.]+@[\w.]+\.\w+/) ||
record.age < 0 || record.age > 150
}
println "\nFirst invalid record: ${firstInvalid}"
// Validate a list of required fields
def requiredFields = ['name', 'email', 'phone', 'address']
def formData = [name: 'Alice', email: 'alice@test.com', phone: '', address: null]
def missingField = requiredFields.find { field ->
!formData[field] // null, empty string, false are all falsy
}
println "First missing field: ${missingField}"
Output
First empty name: index 1, email: bob@test.com First bad email: Charlie -> invalid-email First bad age: Diana -> -5 First invalid record: [name:, email:bob@test.com, age:35] First missing field: phone
What happened here: Validation is another excellent use case for find(). Instead of collecting all errors (where findAll() is more appropriate), sometimes you just need to know the first problem. This is common in fail-fast validation where you show one error at a time. The last example checks required fields – using Groovy’s truthiness, empty strings and null values are both considered “missing.”
find() vs findAll() vs any()
These three methods are related but serve different purposes. Here’s when to use each one:
| Method | Returns | Use When | Short-circuits? |
|---|---|---|---|
find() | First matching element (or null) | You need the actual first match | Yes |
findAll() | List of all matching elements | You need every match | No |
any() | boolean (true/false) | You only need to know if a match exists | Yes |
find() vs findAll() vs any()
def numbers = [3, 7, 12, 5, 18, 2, 25, 9, 14]
// find() - first match
def first = numbers.find { it > 10 }
println "find(): ${first}" // 12
// findAll() - all matches
def all = numbers.findAll { it > 10 }
println "findAll(): ${all}" // [12, 18, 25, 14]
// any() - just true/false
def exists = numbers.any { it > 10 }
println "any(): ${exists}" // true
// Performance tip: don't use findAll() when you only need first or boolean
// Bad: numbers.findAll { it > 10 }.first() // scans entire list
// Good: numbers.find { it > 10 } // stops at first match
// Bad: numbers.findAll { it > 10 }.size() > 0 // scans entire list
// Good: numbers.any { it > 10 } // stops at first match
Output
find(): 12 findAll(): [12, 18, 25, 14] any(): true
The bottom line: find() and any() both short-circuit (stop early), while findAll() always scans the entire collection. If you only need the first match, always use find(), not findAll().first().
Related Methods: findResult(), findIndexOf()
Groovy provides a couple of useful cousins to find(). Here they are.
findResult() – Find and Transform in One Step
The findResult() method combines finding and transforming. Instead of returning the matching element, it returns the result of the closure for the first non-null return value.
findResult() Examples
def numbers = [1, 2, 3, 4, 5]
// find() returns the element
def found = numbers.find { it > 3 }
println "find(): ${found}" // 4
// findResult() returns the closure's result
def result = numbers.findResult { it > 3 ? "Found: ${it}" : null }
println "findResult(): ${result}" // Found: 4
// Practical: Parse and find in one step
def lines = ['name=Alice', 'age=thirty', 'score=95', 'level=5']
def firstNumericValue = lines.findResult { line ->
def parts = line.split('=')
parts[1].isNumber() ? parts[1].toInteger() : null
}
println "First numeric value: ${firstNumericValue}"
// findResult with a default value
def noMatch = numbers.findResult('default') { it > 100 ? it : null }
println "With default: ${noMatch}"
Output
find(): 4 findResult(): Found: 4 First numeric value: 95 With default: default
findIndexOf() – Get the Index, Not the Element
Sometimes you don’t need the element itself – you need to know where it is. That’s what findIndexOf() does.
findIndexOf() Examples
def fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry']
// Find index of first fruit starting with 'c'
def index = fruits.findIndexOf { it.startsWith('c') }
println "Index of first 'c' fruit: ${index}" // 2
println "Element: ${fruits[index]}" // cherry
// Find from a specific starting index
def index2 = fruits.findIndexOf(3) { it.length() > 4 }
println "Index of first long fruit from index 3: ${index2}"
// Returns -1 when not found
def notFound = fruits.findIndexOf { it.startsWith('z') }
println "Not found index: ${notFound}"
// Combining with other operations
def numbers = [10, 25, 30, 45, 50, 65, 70]
def idx = numbers.findIndexOf { it > 40 }
println "Elements from first > 40: ${numbers[idx..-1]}"
Output
Index of first 'c' fruit: 2 Element: cherry Index of first long fruit from index 3: 4 Not found index: -1 Elements from first > 40: [45, 50, 65, 70]
Notice that findIndexOf() returns -1 when nothing matches (similar to Java’s indexOf()), unlike find() which returns null. You can also pass a starting index as the first argument to search from a specific position.
Null Safety with find()
Since find() returns null when nothing matches, handling null results is critical. Here are the best patterns:
Null Safety Patterns
def items = ['apple', 'banana', 'cherry']
// DANGEROUS - NullPointerException if no match
// items.find { it.startsWith('z') }.toUpperCase() // NPE!
// Pattern 1: Safe navigation operator (?.)
def result1 = items.find { it.startsWith('z') }?.toUpperCase()
println "Safe nav: ${result1}"
// Pattern 2: Elvis operator (?:) for default value
def result2 = items.find { it.startsWith('z') } ?: 'not found'
println "Elvis: ${result2}"
// Pattern 3: Combine both
def result3 = items.find { it.startsWith('z') }?.toUpperCase() ?: 'NOT FOUND'
println "Combined: ${result3}"
// Pattern 4: Explicit null check
def found = items.find { it.startsWith('b') }
if (found) {
println "Found: ${found.toUpperCase()}"
} else {
println "Nothing found"
}
// Pattern 5: with findResult() - avoids null entirely
def result5 = items.findResult { it.startsWith('z') ? it.toUpperCase() : null } ?: 'NOTHING'
println "findResult: ${result5}"
// Careful with falsy values!
def mixedList = [0, '', false, null, 42, 'hello']
def zeroResult = mixedList.find { it == 0 }
println "\nfind 0: ${zeroResult}"
println "Is null? ${zeroResult == null}" // false - we found 0
println "Is falsy? ${!zeroResult}" // true! 0 is falsy
// Safe check for find() result when 0 or false could be valid
def safeCheck = mixedList.find { it == 0 }
println "Explicit null check: ${safeCheck != null}" // true - 0 was found
Output
Safe nav: null Elvis: not found Combined: NOT FOUND Found: BANANA findResult: NOTHING find 0: 0 Is null? false Is falsy? true Explicit null check: true
The safe navigation operator ?. and the Elvis operator ?: are your best friends when working with find(). But be careful with the falsy values gotcha: if your collection contains 0, false, or an empty string, the Elvis operator will treat a valid match as “not found.” In those cases, always use an explicit != null check.
Performance Considerations
The find() method is efficient by design because it short-circuits. But there are still a few things to keep in mind:
Performance Tips
def largeList = (1..1_000_000).toList()
// GOOD: find() stops at first match - O(1) in best case
def found = largeList.find { it == 42 }
println "Found early: ${found}"
// BAD: findAll() scans entire list, then takes first
// def bad = largeList.findAll { it > 999_990 }.first()
// GOOD: find() stops as soon as condition is met
def good = largeList.find { it > 999_990 }
println "Found late: ${good}"
// TIP: Put the most likely match condition first in OR chains
// If most items are in 'Electronics', check that first
def products = [
[cat: 'Electronics', name: 'Phone'],
[cat: 'Books', name: 'Novel'],
[cat: 'Electronics', name: 'Laptop']
]
def result = products.find { it.cat == 'Electronics' }
println "First electronics: ${result.name}"
// TIP: For repeated lookups, convert to a Map first
def users = (1..100).collect { [id: it, name: "User${it}"] }
// Slow for repeated lookups: O(n) each time
// users.find { it.id == 50 }
// users.find { it.id == 75 }
// Fast: convert to map once, then O(1) lookups
def userMap = users.collectEntries { [it.id, it] }
println "Map lookup: ${userMap[50].name}"
Output
Found early: 42 Found late: 999991 First electronics: Phone Map lookup: User50
Complexity: find() is O(n) in the worst case (element is last or not found), but O(1) in the best case (element is first). If you need to do repeated lookups on the same collection, convert it to a Map with collectEntries first for O(1) lookups.
Common Pitfalls
Pitfall 1: Forgetting find() Returns null
Pitfall: Null Return
def items = [1, 2, 3]
// This will throw NullPointerException!
try {
def result = items.find { it > 10 }.toString()
} catch (NullPointerException e) {
println "NPE caught! Always use ?. after find()"
}
// Correct way
def result = items.find { it > 10 }?.toString() ?: 'nothing'
println "Safe result: ${result}"
Output
NPE caught! Always use ?. after find() Safe result: nothing
Pitfall 2: Confusing find() with findAll()
Pitfall: find() vs findAll()
def numbers = [2, 4, 6, 8, 10]
// find() returns a single element
def single = numbers.find { it > 5 }
println "find() type: ${single.getClass().name} -> ${single}"
// findAll() returns a list
def multiple = numbers.findAll { it > 5 }
println "findAll() type: ${multiple.getClass().name} -> ${multiple}"
// Common mistake: treating find() result as a list
// numbers.find { it > 5 }.each { ... } // NPE or wrong behavior!
Output
find() type: java.lang.Integer -> 6 findAll() type: java.util.ArrayList -> [6, 8, 10]
Pitfall 3: Truthy Gotchas in the Closure
Pitfall: Truthy Values
def items = ['hello', '', 'world', null, 'groovy']
// This does NOT find the empty string - empty string is falsy!
def found = items.find() // finds first truthy element
println "First truthy: ${found}"
// To actually find an empty string, use a closure
def empty = items.find { it != null && it.isEmpty() }
println "First empty: '${empty}'"
// Be explicit about what "truthy" means in your context
def scores = [0, 85, 92, 0, 77]
def firstScore = scores.find() // Skips 0 because it's falsy!
println "First truthy score: ${firstScore}"
def actualFirst = scores.find { it != null } // 0 is not null
println "Actual first non-null: ${actualFirst}"
Output
First truthy: hello First empty: '' First truthy score: 85 Actual first non-null: 0
The biggest pitfall with find() is Groovy’s truth system. Calling find() without a closure uses default truthiness – so 0, "", false, and null are all skipped. When those values are legitimate data, always write an explicit closure with a != null check.
Conclusion
The Groovy find() method is one of those tools you’ll reach for constantly once you know it exists. It replaces verbose loops with a clean, declarative one-liner that says exactly what you mean: “give me the first element that matches this condition.”
We worked through 10 examples covering lists, maps, strings, custom objects, method chaining, record searching, configuration lookups, and data validation. Along the way, we also explored the findResult() and findIndexOf() companion methods, compared find() to findAll() and any(), and covered the null safety patterns you’ll need in production code.
Next up in the series, we’ll cover findAll() – which does the same thing but returns all matching elements instead of just the first one. If you haven’t already, also check out the each() loop tutorial for the basics of iterating through collections.
Summary
find()returns the first matching element, ornullif nothing matches- It works on Lists, Maps, Arrays, Strings, and any Iterable
- Always use
?.or?:when accessing the result to avoid NullPointerException - Use
find()for first match,findAll()for all matches,any()for existence check findResult()combines finding and transforming;findIndexOf()returns the position- Watch out for Groovy truthiness –
0,"", andfalseare all falsy
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 findAll() – Find All Matching Elements
Frequently Asked Questions
What does find() return when no element matches?
The Groovy find() method returns null when no element in the collection matches the closure condition. This is why you should always use the safe navigation operator (?.) or the Elvis operator (?:) when accessing properties on the result. For example: list.find { it > 100 }?.toString() ?: ‘not found’.
What is the difference between find() and findAll() in Groovy?
find() returns only the first matching element (or null), while findAll() returns a list of all matching elements (or an empty list). find() short-circuits after the first match, making it more efficient when you only need one result. Use find() for single lookups and findAll() when you need every match.
Can I use find() on a Groovy Map?
Yes. When you call find() on a Map, the closure receives either a Map.Entry (single parameter) or key and value (two parameters). The method returns the first matching Map.Entry, so you can access both .key and .value on the result. Example: map.find { k, v -> v > 90 } returns an entry like Alice=95.
How does find() work on strings in Groovy?
Groovy’s find() on strings (CharSequence) works with regex patterns. You pass a regex and it returns the first matching substring. For example, ‘Hello 42 World’.find(/\d+/) returns ’42’. This is different from find() on collections, which uses a closure predicate. You can also call toList() on a string to search character by character.
Is Groovy find() the same as Java Stream findFirst()?
They are similar in purpose but different in API. Groovy’s find() takes a closure predicate and returns the element directly (or null). Java’s Stream.filter().findFirst() returns an Optional. Groovy’s version is more concise – list.find { it > 10 } vs list.stream().filter(x -> x > 10).findFirst().orElse(null). Both short-circuit on the first match.
Related Posts
Previous in Series: Groovy Switch Statement – Advanced Pattern Matching
Next in Series: Groovy findAll() – Find All Matching Elements
Related Topics You Might Like:
- Groovy each() Loop – Iterate Collections Like a Pro
- Groovy collect() – Transform Collections Easily
- Groovy Closures – The Complete Guide
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment