10 Useful Groovy find() Method Examples to Locate First Match

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.

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 null when 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:

MethodReturnsUse WhenShort-circuits?
find()First matching element (or null)You need the actual first matchYes
findAll()List of all matching elementsYou need every matchNo
any()boolean (true/false)You only need to know if a match existsYes

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().

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, or null if 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, "", and false are 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.

Previous in Series: Groovy Switch Statement – Advanced Pattern Matching

Next in Series: Groovy findAll() – Find All Matching Elements

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 *