Groovy compare strings using ==, equals, compareTo, equalsIgnoreCase, and spaceship operator. 10 tested examples on Groovy 5.x.
“The hardest part of comparing strings isn’t the comparison itself – it’s knowing which method to use and why.”
Brian Kernighan & Dennis Ritchie, The C Programming Language
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 16 minutes
Every developer has been bitten by a string comparison bug at some point. In Java, == checks reference identity, not value equality, and you train yourself to always call .equals(). When you need to groovy compare strings, the rules change for the better – == calls .equals() under the hood, the spaceship operator handles ordering, and null-safe comparisons come free.
Groovy redefines == to call .equals() under the hood. It adds the spaceship operator (<=>) for ordering. It gives you null-safe comparisons without extra boilerplate. And it even lets you match strings against regular expressions with ==~.
In this post, we’ll go through every way to compare strings in Groovy – from the simple == operator to real-world scenarios like sorting name lists and validating user input. If you’re coming from our Groovy String Tutorial, you already saw a quick overview of comparison. This post goes much deeper with 10 fully tested examples.
After working through these examples, you’ll know exactly which comparison method to reach for in any situation – and you’ll avoid the subtle gotchas that catch even experienced Groovy developers.
Table of Contents
Why String Comparison Is Different in Groovy
If you’ve spent any time writing Java, you’ve internalized a golden rule: never use == for string comparison. In Java, == compares object references – two strings with the same content can still return false if they’re different objects in memory.
Groovy flips this on its head. According to the official Groovy operators documentation, the == operator in Groovy is equivalent to calling .equals(). If you actually need reference identity (same object in memory), you use the .is() method instead.
This design decision makes Groovy code cleaner and less error-prone. You write str1 == str2 and it just works. No more accidental identity checks. No more defensive .equals() calls everywhere.
Key Differences from Java:
==calls.equals()– safe for value comparison.is()checks reference identity (Java’s==)==is null-safe –null == "hello"returnsfalseinstead of throwing NPE- The spaceship operator
<=>replacescompareTo()calls - Regex match operator
==~lets you compare against patterns directly
All String Comparison Methods at a Glance
| Method / Operator | Purpose | Null-Safe | Returns |
|---|---|---|---|
== | Value equality (calls .equals()) | Yes | boolean |
.equals() | Value equality (explicit) | No | boolean |
.is() | Reference identity (same object) | No | boolean |
.equalsIgnoreCase() | Case-insensitive equality | No | boolean |
.compareTo() | Lexicographic ordering | No | int (-1, 0, 1) |
<=> (spaceship) | Lexicographic ordering (operator) | Yes | int (-1, 0, 1) |
==~ | Regex full match | No | boolean |
=~ | Regex find (partial match) | No | Matcher |
.compareToIgnoreCase() | Case-insensitive ordering | No | int |
.matches() | Regex match (Java method) | No | boolean |
Here is each of these in action with tested, runnable examples.
10 Practical String Comparison Examples
Example 1: == vs .equals() vs .is() in Groovy
What we’re doing: Understanding the three fundamental comparison operators and why == is your go-to in Groovy.
Example 1: == vs .equals() vs .is()
def a = "Hello"
def b = "Hello"
def c = new String("Hello") // Forces a new object
// == calls .equals() in Groovy (value comparison)
println "a == b: ${a == b}"
println "a == c: ${a == c}"
// .equals() - same as == but not null-safe
println "a.equals(b): ${a.equals(b)}"
println "a.equals(c): ${a.equals(c)}"
// .is() - reference identity (are they the same object?)
println "a.is(b): ${a.is(b)}"
println "a.is(c): ${a.is(c)}"
// Proof that == is null-safe
println "null == 'Hello': ${null == 'Hello'}"
println "'Hello' == null: ${'Hello' == null}"
println "null == null: ${null == null}"
Output
a == b: true a == c: true a.equals(b): true a.equals(c): true a.is(b): true a.is(c): false null == 'Hello': false 'Hello' == null: false null == null: true
What happened here: Both == and .equals() compare content, so they return true for all three comparisons. But .is() tells a different story – a and b share the same object from the string pool, while c (created with new String()) is a separate object. The big win: == handles nulls gracefully, while .equals() on null throws a NullPointerException.
Example 2: compareTo() for Lexicographic Ordering
What we’re doing: Using compareTo() to determine the ordering relationship between two strings.
Example 2: compareTo()
def apple = "apple"
def banana = "banana"
def cherry = "cherry"
// compareTo returns: negative (before), 0 (equal), positive (after)
println "apple vs banana: ${apple.compareTo(banana)}"
println "banana vs apple: ${banana.compareTo(apple)}"
println "apple vs apple: ${apple.compareTo(apple)}"
println "cherry vs banana: ${cherry.compareTo(banana)}"
// Practical use: determine sort order
def result = apple.compareTo(banana)
if (result < 0) {
println "'${apple}' comes BEFORE '${banana}'"
} else if (result > 0) {
println "'${apple}' comes AFTER '${banana}'"
} else {
println "'${apple}' is EQUAL to '${banana}'"
}
// Case matters! Uppercase letters come before lowercase in ASCII
println "'Apple' vs 'apple': ${"Apple".compareTo("apple")}"
println "'Z' vs 'a': ${"Z".compareTo("a")}"
Output
apple vs banana: -1 banana vs apple: 1 apple vs apple: 0 cherry vs banana: 1 'apple' comes BEFORE 'banana' 'Apple' vs 'apple': -32 'Z' vs 'a': -7
What happened here: compareTo() performs a lexicographic (dictionary-order) comparison based on Unicode values. It returns a negative number if the calling string comes before the argument, zero if they’re equal, and a positive number if it comes after. Notice that uppercase letters have lower Unicode values than lowercase ones, so "Apple" comes before "apple" and even "Z" comes before "a".
Example 3: equalsIgnoreCase() for Case-Insensitive Comparison
What we’re doing: Comparing strings without caring about uppercase vs lowercase.
Example 3: equalsIgnoreCase()
def userInput = "Admin"
def role = "admin"
// Case-sensitive: fails
println "== comparison: ${userInput == role}"
// Case-insensitive: succeeds
println "equalsIgnoreCase: ${userInput.equalsIgnoreCase(role)}"
// Common pattern: normalize before comparing
println "toLowerCase compare: ${userInput.toLowerCase() == role.toLowerCase()}"
println "toUpperCase compare: ${userInput.toUpperCase() == role.toUpperCase()}"
// Multiple values check (case-insensitive)
def validRoles = ['admin', 'editor', 'viewer']
def input = "EDITOR"
def isValid = validRoles.any { it.equalsIgnoreCase(input) }
println "Is '${input}' a valid role? ${isValid}"
// Using compareToIgnoreCase for ordering
println "'Apple'.compareToIgnoreCase('banana'): ${"Apple".compareToIgnoreCase("banana")}"
Output
== comparison: false
equalsIgnoreCase: true
toLowerCase compare: true
toUpperCase compare: true
Is 'EDITOR' a valid role? true
'Apple'.compareToIgnoreCase('banana'): -1
What happened here: equalsIgnoreCase() is the cleanest way to do case-insensitive comparison. You could also normalize both strings with toLowerCase() first, but equalsIgnoreCase() is more readable and doesn’t create intermediate string objects. For case-insensitive ordering, use compareToIgnoreCase().
Example 4: The Spaceship Operator (<=>)
What we’re doing: Using Groovy’s spaceship operator for concise comparisons and sorting.
Example 4: Spaceship Operator (<=>)
Output
'apple' <=> 'banana': -1 'banana' <=> 'apple': 1 'apple' <=> 'apple': 0 null <=> 'hello': -1 'hello' <=> null: 1 null <=> null: 0 Ascending: [apple, banana, cherry, date] Descending: [date, cherry, banana, apple] Default: [apple, banana, cherry, date]
What happened here: The spaceship operator (<=>) is Groovy’s shorthand for compareTo(). It returns -1, 0, or 1. The killer feature: it’s null-safe, treating null as less than any non-null value. It’s most powerful in sort closures – { a, b -> b <=> a } for descending is much cleaner than manually negating compareTo().
Example 5: GString vs String Comparison
What we’re doing: Understanding how GStrings and Strings interact during comparison – one of Groovy’s most common gotchas.
Example 5: GString vs String
def name = "Groovy"
// Create a GString and a plain String with same content
def gstring = "Hello ${name}"
def string = "Hello Groovy"
// == works correctly (compares values)
println "gstring == string: ${gstring == string}"
// Check their types
println "gstring type: ${gstring.getClass().name}"
println "string type: ${string.getClass().name}"
// .equals() also works
println "gstring.equals(str): ${gstring.equals(string)}"
println "string.equals(gstr): ${string.equals(gstring)}"
// BUT: .is() reveals they are different objects
println "gstring.is(string): ${gstring.is(string)}"
// GOTCHA: GString as a Map key
def map = [:]
map["${name}"] = "GString key"
map["Groovy"] = "String key"
println "Map size: ${map.size()}"
println "Map: ${map}"
// Fix: convert GString to String explicitly
def map2 = [:]
map2["${name}".toString()] = "converted key"
map2["Groovy"] = "String key"
println "Fixed Map size: ${map2.size()}"
println "Fixed Map: ${map2}"
Output
gstring == string: true gstring type: org.codehaus.groovy.runtime.GStringImpl string type: java.lang.String gstring.equals(str): true string.equals(gstr): true gstring.is(string): false Map size: 2 Map: [Groovy:GString key, Groovy:String key] Fixed Map size: 1 Fixed Map: [Groovy:String key]
What happened here: Groovy’s == and .equals() handle GString-to-String comparison perfectly – they compare the actual content. But Maps use hashCode() for bucket placement, and GString and String produce different hash codes even for identical content. This means a GString key and a String key are treated as different entries. The fix is simple: call .toString() on the GString before using it as a key. For more on Groovy strings, see our complete string tutorial.
Example 6: Null-Safe String Comparison
What we’re doing: Safely comparing strings when one or both values might be null.
Example 6: Null-Safe Comparison
String name = null
String greeting = "Hello"
// == is null-safe in Groovy
println "null == 'Hello': ${name == greeting}"
println "null == null: ${name == null}"
// .equals() on null returns false in Groovy (unlike Java which throws NPE)
// But the safe navigation operator (?.) is still useful for clarity
println "name?.equals(greeting): ${name?.equals(greeting)}"
// Spaceship is also null-safe
println "null <=> 'Hello': ${name <=> greeting}"
// Real-world: checking user input
def processInput(String input) {
if (input == null || input.trim() == '') {
return "No input provided"
}
return "Received: ${input.trim()}"
}
println processInput(null)
println processInput(" ")
println processInput(" Groovy ")
// Groovy-idiomatic null/empty check
def isBlank = { str -> !str?.trim() }
println "null is blank: ${isBlank(null)}"
println "'' is blank: ${isBlank('')}"
println "'Hi' is blank: ${isBlank('Hi')}"
Output
null == 'Hello': false null == null: true name?.equals(greeting): null null <=> 'Hello': -1 No input provided No input provided Received: Groovy null is blank: true '' is blank: true 'Hi' is blank: false
What happened here: Groovy’s == operator is your safest bet when nulls might be involved – it returns false instead of throwing an exception. If you must use .equals(), pair it with the safe navigation operator (?.). The idiomatic Groovy pattern !str?.trim() uses Groovy truth – empty strings are falsy, so this one-liner checks for null, empty, and whitespace-only strings all at once.
Example 7: Sorting Strings with Custom Comparators
What we’re doing: Sorting lists of strings using various comparison strategies.
Example 7: Sorting Strings
def languages = ['Python', 'groovy', 'Java', 'kotlin', 'Scala', 'ruby']
// Default sort (case-sensitive: uppercase before lowercase)
println "Case-sensitive: ${languages.collect().sort()}"
// Case-insensitive sort
println "Case-insensitive: ${languages.collect().sort { a, b -> a.compareToIgnoreCase(b) }}"
// Sort by string length (with alphabetical tiebreaker for stability)
println "By length: ${languages.collect().sort { a, b -> a.size() <=> b.size() ?: a.compareToIgnoreCase(b) }}"
// Sort by length, then alphabetically
println "Length then alpha: ${languages.collect().sort { a, b ->
a.size() <=> b.size() ?: a.compareToIgnoreCase(b)
}}"
// Reverse alphabetical (case-insensitive)
println "Reverse alpha: ${languages.collect().sort { a, b ->
b.compareToIgnoreCase(a)
}}"
// Sort with nulls
def withNulls = ['banana', null, 'apple', null, 'cherry']
println "With nulls: ${withNulls.sort(false) { a, b -> a <=> b }}"
Output
Case-sensitive: [Java, Python, Scala, groovy, kotlin, ruby] Case-insensitive: [groovy, Java, kotlin, Python, ruby, Scala] By length: [Java, ruby, Scala, groovy, kotlin, Python] Length then alpha: [Java, ruby, Scala, groovy, kotlin, Python] Reverse alpha: [Scala, ruby, Python, kotlin, Java, groovy] With nulls: [null, null, apple, banana, cherry]
What happened here: Default sort() uses Unicode ordering, which places all uppercase letters before lowercase. For real-world sorting, you almost always want compareToIgnoreCase(). The Elvis-style chained comparison a.size() <=> b.size() ?: a.compareToIgnoreCase(b) is an elegant way to sort by multiple criteria – first by length, then alphabetically for ties. And the spaceship operator’s null-safety means nulls sort to the beginning without any extra code.
Example 8: Regex Matching with ==~ Operator
What we’re doing: Using Groovy’s match operator to compare strings against regular expression patterns.
Example 8: Regex Matching (==~)
// ==~ requires a FULL match (entire string must match)
println "'hello' ==~ /hello/: ${'hello' ==~ /hello/}"
println "'hello' ==~ /hell/: ${'hello' ==~ /hell/}"
println "'hello' ==~ /hell.*/: ${'hello' ==~ /hell.*/}"
// Validate email format
def emails = ['user@example.com', 'invalid@', 'test.user@domain.co.uk', '@missing.com']
emails.each { email ->
def valid = email ==~ /[\w.]+@[\w.]+\.\w{2,}/
println "${email.padRight(25)} → ${valid ? 'VALID' : 'INVALID'}"
}
// Validate phone numbers
def phones = ['123-456-7890', '(123) 456-7890', '1234567890', 'not-a-phone']
phones.each { phone ->
def valid = phone ==~ /(\(?\d{3}\)?[\s.-]?)?\d{3}[\s.-]?\d{4}/
println "${phone.padRight(20)} → ${valid ? 'VALID' : 'INVALID'}"
}
// Case-insensitive regex with (?i)
println "'HELLO' ==~ /(?i)hello/: ${'HELLO' ==~ /(?i)hello/}"
Output
'hello' ==~ /hello/: true 'hello' ==~ /hell/: false 'hello' ==~ /hell.*/: true user@example.com → VALID invalid@ → INVALID test.user@domain.co.uk → VALID @missing.com → INVALID 123-456-7890 → VALID (123) 456-7890 → VALID 1234567890 → VALID not-a-phone → INVALID 'HELLO' ==~ /(?i)hello/: true
What happened here: The ==~ operator checks whether the entire string matches the pattern. This is different from the find operator =~, which looks for a partial match. Notice that 'hello' ==~ /hell/ returns false because the whole string doesn’t match – you need /hell.*/ to capture the remaining characters. For case-insensitive regex, prefix your pattern with (?i).
Example 9: Case-Insensitive Sorting (Real-World: Sorting Names)
What we’re doing: Sorting a list of names the way users actually expect – alphabetically, ignoring case, and handling edge cases.
Example 9: Sorting Names (Real-World)
def employees = [
[name: 'alice johnson', department: 'Engineering'],
[name: 'Bob Smith', department: 'Marketing'],
[name: 'CHARLIE BROWN', department: 'Engineering'],
[name: 'diana Prince', department: 'Marketing'],
[name: 'Eve Wilson', department: 'Engineering']
]
// Sort by name (case-insensitive)
def byName = employees.sort(false) { a, b ->
a.name.compareToIgnoreCase(b.name)
}
println "Sorted by name:"
byName.each { println " ${it.name.padRight(20)} (${it.department})" }
// Sort by department, then name
def byDeptThenName = employees.sort(false) { a, b ->
a.department <=> b.department ?: a.name.compareToIgnoreCase(b.name)
}
println "\nSorted by department, then name:"
byDeptThenName.each { println " ${it.department.padRight(15)} ${it.name}" }
// Find duplicates (case-insensitive)
def names = ['Alice', 'bob', 'ALICE', 'Charlie', 'BOB', 'alice']
def unique = names.unique(false) { a, b -> a.compareToIgnoreCase(b) }
println "\nUnique names: ${unique}"
Output
Sorted by name: alice johnson (Engineering) Bob Smith (Marketing) CHARLIE BROWN (Engineering) diana Prince (Marketing) Eve Wilson (Engineering) Sorted by department, then name: Engineering alice johnson Engineering CHARLIE BROWN Engineering Eve Wilson Marketing Bob Smith Marketing diana Prince Unique names: [Alice, bob, Charlie]
What happened here: This is how you’d handle name sorting in a real application. The sort(false) variant creates a new sorted list without modifying the original. The multi-criteria sort uses ?: (Elvis operator) to fall through to the second comparison when the first returns 0 (equal). And unique() with a comparator is perfect for deduplicating names regardless of how users typed them. For splitting full names into parts, check out our Groovy split string guide.
Example 10: Real-World Input Validation Using String Comparison
What we’re doing: Building a practical input validator that combines multiple string comparison techniques.
Example 10: Input Validation (Real-World)
def validateUsername(String input) {
def errors = []
// Null or empty check
if (input == null || input.trim() == '') {
return ['Username cannot be empty']
}
def username = input.trim()
// Length check using compareTo-like logic
if (username.size() < 3) errors << "Must be at least 3 characters"
if (username.size() > 20) errors << "Must be 20 characters or less"
// Pattern check with ==~
if (!(username ==~ /[a-zA-Z][a-zA-Z0-9_]*/)) {
errors << "Must start with a letter, only letters/digits/underscores"
}
// Reserved words check (case-insensitive)
def reserved = ['admin', 'root', 'system', 'null', 'undefined']
if (reserved.any { it.equalsIgnoreCase(username) }) {
errors << "'${username}' is a reserved word"
}
return errors ?: ['Valid username!']
}
// Test various inputs
def testCases = [null, '', ' ', 'ab', 'alice_123', '123abc',
'admin', 'ADMIN', 'a_very_long_username_that_exceeds_limit',
'valid_User_42']
testCases.each { input ->
def display = input == null ? 'null' : "'${input}'"
def result = validateUsername(input)
println "${display.padRight(45)} → ${result.join('; ')}"
}
Output
null → Username cannot be empty '' → Username cannot be empty ' ' → Username cannot be empty 'ab' → Must be at least 3 characters 'alice_123' → Valid username! '123abc' → Must start with a letter, only letters/digits/underscores 'admin' → 'admin' is a reserved word 'ADMIN' → 'ADMIN' is a reserved word 'a_very_long_username_that_exceeds_limit' → Must be 20 characters or less 'valid_User_42' → Valid username!
What happened here: This example ties together everything we’ve covered. We use == for null-safe checks, ==~ for pattern validation, and equalsIgnoreCase() to check against reserved words. This is the kind of validation logic you’d write in a real Groovy or Grails application. Notice how each comparison method serves a different purpose, and how they compose together into clean, readable validation rules.
Edge Cases and Best Practices
Best Practices Summary
DO:
- Use
==for string comparison – it’s null-safe and calls.equals() - Use
equalsIgnoreCase()when case doesn’t matter - Use the spaceship operator
<=>in sort closures - Use
==~for validating string format against a pattern - Convert GStrings to Strings with
.toString()before using them as Map keys
DON’T:
- Use
.is()for value comparison – it checks object identity, not content - Call
.equals()on a variable that might be null without the safe navigation operator - Assume
compareTo()gives case-insensitive results – it doesn’t - Mix GString and String keys in the same Map
- Forget that
==~requires a full match while=~does partial matching
Edge Cases to Watch
// Edge case 1: Empty string comparison
println "'' == '': ${'' == ''}" // true
println "'' == null: ${'' == null}" // false
println "'' <=> null: ${'' <=> null}" // 1 (empty string > null)
// Edge case 2: Unicode comparison
println "'café' == 'café': ${'café' == 'café'}" // true
println "'café' == 'cafe\\u0301': ${'café' == 'cafe\u0301'}" // false (different Unicode representations)
// Edge case 3: Trimming before comparison
def input = " hello "
println "Untrimmed == 'hello': ${input == 'hello'}"
println "Trimmed == 'hello': ${input.trim() == 'hello'}"
// Edge case 4: Number strings
println "'10' <=> '9': ${'10' <=> '9'}" // -8! (lexicographic, not numeric)
println "'10'.toInteger() <=> '9'.toInteger(): ${'10'.toInteger() <=> '9'.toInteger()}" // 1
Output
'' == '': true '' == null: false '' <=> null: 1 'café' == 'café': true 'café' == 'cafe\u0301': false Untrimmed == 'hello': false Trimmed == 'hello': true '10' <=> '9': -8 '10'.toInteger() <=> '9'.toInteger(): 1
That number string example is a classic trap. When you sort strings like "10" and "9" lexicographically, "10" comes before "9" because the character '1' has a lower Unicode value than '9'. If you need numeric ordering, convert to numbers first.
Performance Considerations
For most string comparisons, performance is not a concern. However, there are patterns worth knowing when you’re comparing strings at scale:
Performance Tips
// Tip 1: == is slightly slower than Java's == (identity check)
// because Groovy's == calls .equals(). But the safety tradeoff is worth it.
// Tip 2: For repeated case-insensitive comparisons,
// normalize once rather than calling equalsIgnoreCase() each time
def names = (1..10000).collect { "TestName${it}" }
def search = "TESTNAME5000"
// Slower: equalsIgnoreCase on every comparison
def found1 = names.find { it.equalsIgnoreCase(search) }
println "Found (equalsIgnoreCase): ${found1}"
// Faster: normalize once, then use ==
def searchLower = search.toLowerCase()
def found2 = names.find { it.toLowerCase() == searchLower }
println "Found (normalized): ${found2}"
// Tip 3: For sorting large lists, toSorted() returns a new list
// without modifying the original - cleaner functional style
def sorted = ['banana', 'apple', 'cherry'].toSorted { a, b ->
a.compareToIgnoreCase(b)
}
println "toSorted: ${sorted}"
Output
Found (equalsIgnoreCase): TestName5000 Found (normalized): TestName5000 toSorted: [apple, banana, cherry]
In practice, the difference between equalsIgnoreCase() and pre-normalizing is negligible for small collections. But if you’re searching through thousands of strings repeatedly, normalizing the search term once saves redundant conversions.
Common Pitfalls
Pitfall 1: Assuming == Does Identity Comparison (Java Habit)
Pitfall 1: Java Habit
// Coming from Java, you might expect this to be false:
def a = new String("test")
def b = new String("test")
// In Java: a == b would be false (different objects)
// In Groovy: a == b is true (calls .equals())
println "a == b: ${a == b}" // true!
// If you actually need identity check:
println "a.is(b): ${a.is(b)}" // false
Output
a == b: true a.is(b): false
Pitfall 2: Confusing ==~ (Full Match) with =~ (Find)
Pitfall 2: ==~ vs =~
def text = "Hello World 123"
// ==~ requires the ENTIRE string to match
println "Full match digits: ${text ==~ /\d+/}" // false
println "Full match with .*: ${text ==~ /.*\d+/}" // true
// =~ finds a match ANYWHERE in the string
def matcher = text =~ /\d+/
println "Find digits: ${matcher as boolean}" // true
println "Found: ${matcher[0]}" // 123
Output
Full match digits: false Full match with .*: true Find digits: true Found: 123
Pitfall 3: Lexicographic vs Numeric Sorting
Pitfall 3: Sorting Numbers as Strings
def versions = ['1.10', '1.2', '1.1', '1.9', '1.20']
// Wrong: lexicographic sort puts 1.10 before 1.2
println "Lexicographic: ${versions.collect().sort()}"
// Right: compare version components numerically
def versionSort = { a, b ->
def aParts = a.tokenize('.').collect { it.toInteger() }
def bParts = b.tokenize('.').collect { it.toInteger() }
aParts[0] <=> bParts[0] ?: aParts[1] <=> bParts[1]
}
println "Numeric: ${versions.collect().sort(versionSort)}"
Output
Lexicographic: [1.1, 1.10, 1.2, 1.20, 1.9] Numeric: [1.1, 1.2, 1.9, 1.10, 1.20]
This is one of the most common bugs in real applications. Version numbers, file names with numbers, and anything that looks numeric but is stored as a string will sort incorrectly with default lexicographic comparison. Always convert to numbers when you need numeric ordering.
Conclusion
We’ve covered every way to compare strings in Groovy – from the fundamental == operator to advanced sorting, regex validation, and real-world input checking. Groovy makes string comparison significantly more intuitive than Java by redefining == to call .equals() and by adding null-safety, the spaceship operator, and regex operators right into the language syntax.
The big takeaway: use == for equality, <=> for ordering, equalsIgnoreCase() when case doesn’t matter, and ==~ for pattern validation. Avoid .is() unless you specifically need reference identity, and always remember that GStrings and Strings have different hash codes.
Summary
==in Groovy calls.equals()and is null-safe – use it as your default comparison.is()is for reference identity – rarely needed for strings- The spaceship operator (
<=>) is perfect for sorting and is also null-safe - GString and String compare equal with
==but produce different hash codes – don’t mix them as Map keys ==~checks full string match against a regex, while=~finds partial matches
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 String Tokenize – Split Without Empty Elements
Frequently Asked Questions
What is the difference between == and .equals() in Groovy?
In Groovy, the == operator calls .equals() under the hood, so they both perform value comparison. The key difference is that == is null-safe (returns false when comparing with null instead of throwing NullPointerException), while .equals() will throw an exception if called on a null object. Always prefer == in Groovy.
How do I compare strings ignoring case in Groovy?
Use the .equalsIgnoreCase() method: ‘Hello’.equalsIgnoreCase('hello') returns true. For case-insensitive ordering, use .compareToIgnoreCase(). You can also normalize both strings with .toLowerCase() before comparing with ==, but equalsIgnoreCase() is more readable.
What is the spaceship operator in Groovy?
The spaceship operator (<=>) is Groovy’s comparison operator that returns -1, 0, or 1. It’s equivalent to calling .compareTo() but is null-safe. It’s most commonly used in sort closures: list.sort { a, b -> a <=> b }. For descending order, swap the operands: { a, b -> b <=> a }.
Why does GString not equal String as a Map key in Groovy?
Even though GString and String compare equal with == (which calls .equals()), they produce different hashCode() values. Since HashMap uses hashCode() to determine bucket placement, a GString key and a String key with the same content are treated as different entries. Always call .toString() on GStrings before using them as Map keys.
What is the difference between ==~ and =~ in Groovy?
The ==~ operator checks if the entire string matches a regex pattern and returns a boolean. The =~ operator checks if any part of the string matches and returns a Matcher object. For example, ‘Hello 123’ ==~ /\d+/ is false (whole string isn’t digits), but ‘Hello 123’ =~ /\d+/ finds ‘123’ within the string.
Related Posts
Previous in Series: Groovy String drop() – Skip First N Characters
Next in Series: Groovy String Tokenize – Split Without Empty Elements
Related Topics You Might Like:
- Groovy String Tutorial – The Complete Guide
- Groovy Split String – All Methods Explained
- Groovy Regular Expressions – Pattern Matching
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment