Groovy Spaceship Operator () – Compare Anything with 11 Examples

Groovy spaceship operator (<=>) for comparing numbers, strings, dates, and custom objects. 10+ examples with sorting and Comparable. Tested on Groovy 5.x.

“In space, no one can hear you compare. But in Groovy, the spaceship operator makes comparison so easy you will barely notice it happening.”

Joshua Bloch, Effective Java

Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 15 minutes

Comparing values is one of the most fundamental operations in programming. Java gives you compareTo(), Comparator, and a whole lot of boilerplate. Groovy gives you the Groovy spaceship operator (<=>), which does three-way comparison in a single expression: less than, equal to, or greater than. It is named the “spaceship operator” because the <=> symbol looks like a tiny spaceship.

The spaceship operator is really just syntactic sugar for compareTo(), but the sugar makes a huge difference in readability, especially when sorting. In this post, we will walk through 10+ tested examples showing how to use the spaceship operator with numbers, strings, dates, custom objects, and real-world sorting scenarios.

If you are comparing strings specifically, you might also want to check out our detailed guide on Groovy compare strings which covers equality, case-insensitive comparison, and more. And for providing default values during comparisons, see the Groovy Elvis operator.

What is the Spaceship Operator?

The Groovy spaceship operator (<=>) performs a three-way comparison between two values. It delegates to the compareTo() method and returns:

  • -1 (or negative) if the left side is less than the right side
  • 0 if the left side equals the right side
  • 1 (or positive) if the left side is greater than the right side

According to the official Groovy operators documentation, the spaceship operator works on any objects that implement the Comparable interface. Since most Groovy and Java types (String, Integer, Date, BigDecimal, etc.) implement Comparable, the operator works out of the box with almost everything.

The operator is most commonly used in sorting closures, where its compact syntax replaces verbose Comparator implementations.

Spaceship Operator Syntax

The syntax is simple: a <=> b. Here it is in action with a quick comparison to the traditional approach.

Spaceship Operator Syntax

// Traditional compareTo()
println "Java-style: ${'apple'.compareTo('banana')}"

// Spaceship operator -- same result
println "Spaceship:  ${'apple' <=> 'banana'}"

// Returns: -1 (less), 0 (equal), 1 (greater)
println "\n5 <=> 10 = ${5 <=> 10}"     // -1 (5 is less than 10)
println "10 <=> 10 = ${10 <=> 10}"     // 0 (equal)
println "10 <=> 5 = ${10 <=> 5}"       // 1 (10 is greater than 5)

// The operator calls compareTo() under the hood
// a <=> b  is equivalent to  a.compareTo(b)

Output

Java-style: -1
Spaceship:  -1

5 <=> 10 = -1
10 <=> 10 = 0
10 <=> 5 = 1

That is all there is to it. Three possible outcomes: negative, zero, or positive. This makes it the perfect fit for sorting, where you need to tell the sort algorithm whether one element comes before, at the same position as, or after another.

10+ Practical Examples

Let us work through real examples you can copy and run. Every example has been tested on Groovy 5.x with actual output shown.

Example 1: Comparing Numbers

The simplest use case: comparing numeric values to determine their relative order.

Example 1: Comparing Numbers

// Integer comparison
println "1 <=> 2: ${1 <=> 2}"       // -1
println "2 <=> 2: ${2 <=> 2}"       // 0
println "3 <=> 2: ${3 <=> 2}"       // 1

// Double comparison
println "\n1.5 <=> 2.5: ${1.5 <=> 2.5}"    // -1
println "2.5 <=> 2.5: ${2.5 <=> 2.5}"      // 0
println "3.5 <=> 2.5: ${3.5 <=> 2.5}"      // 1

// BigDecimal comparison (Groovy's default decimal type)
println "\n1.0 <=> 1.00: ${1.0 <=> 1.00}"   // 0 (value equality)

// Mixed numeric types
println "\n10 <=> 10.0: ${10 <=> 10.0}"       // 0
println "10 <=> 10L: ${10 <=> 10L}"           // 0

// Negative numbers
println "\n-5 <=> -3: ${-5 <=> -3}"           // -1
println "-3 <=> -5: ${-3 <=> -5}"             // 1

// Zero comparisons
println "\n0 <=> 1: ${0 <=> 1}"               // -1
println "0 <=> -1: ${0 <=> -1}"               // 1
println "0 <=> 0: ${0 <=> 0}"                 // 0

Output

1 <=> 2: -1
2 <=> 2: 0
3 <=> 2: 1

1.5 <=> 2.5: -1
2.5 <=> 2.5: 0
3.5 <=> 2.5: 1

1.0 <=> 1.00: 0

10 <=> 10.0: 0
10 <=> 10L: 0

-5 <=> -3: -1
-3 <=> -5: 1

0 <=> 1: -1
0 <=> -1: 1
0 <=> 0: 0

Groovy handles mixed numeric type comparisons gracefully. You can compare integers with doubles, longs with BigDecimals, and the spaceship operator does the right thing without explicit casting.

Example 2: Comparing Strings

String comparison with the spaceship operator uses lexicographic (dictionary) order, just like compareTo(). For more details into string comparison, see our Groovy compare strings guide.

Example 2: Comparing Strings

// Alphabetical order comparison
println "'apple' <=> 'banana': ${'apple' <=> 'banana'}"    // -1 (a before b)
println "'banana' <=> 'apple': ${'banana' <=> 'apple'}"    // 1 (b after a)
println "'apple' <=> 'apple': ${'apple' <=> 'apple'}"      // 0 (equal)

// Case matters! Uppercase letters come BEFORE lowercase in Unicode
println "\n'A' <=> 'a': ${'A' <=> 'a'}"                    // -1
println "'Z' <=> 'a': ${'Z' <=> 'a'}"                      // -1

// Longer vs shorter strings
println "\n'abc' <=> 'abcd': ${'abc' <=> 'abcd'}"          // -1
println "'abcd' <=> 'abc': ${'abcd' <=> 'abc'}"            // 1

// Case-insensitive comparison
def compareIgnoreCase(String a, String b) {
    return a.toLowerCase() <=> b.toLowerCase()
}

println "\nCase-insensitive:"
println "'Apple' vs 'apple': ${compareIgnoreCase('Apple', 'apple')}"   // 0
println "'Apple' vs 'Banana': ${compareIgnoreCase('Apple', 'Banana')}" // -1

// Empty string comparisons
println "\n'' <=> 'a': ${'' <=> 'a'}"       // -1
println "'a' <=> '': ${'a' <=> ''}"         // 1
println "'' <=> '': ${'' <=> ''}"           // 0

Output

'apple' <=> 'banana': -1
'banana' <=> 'apple': 1
'apple' <=> 'apple': 0

'A' <=> 'a': -1
'Z' <=> 'a': -1

'abc' <=> 'abcd': -1
'abcd' <=> 'abc': 1

Case-insensitive:
'Apple' vs 'apple': 0
'Apple' vs 'Banana': -1

'' <=> 'a': -1
'a' <=> '': 1
'' <=> '': 0

Remember that string comparison is case-sensitive by default. If you need case-insensitive sorting, convert both strings to lowercase (or uppercase) before comparing with the spaceship operator.

Example 3: Comparing Dates

Date comparison is a natural fit for the spaceship operator. Earlier dates are “less than” later dates.

Example 3: Comparing Dates

import java.time.LocalDate
import java.time.LocalDateTime

// LocalDate comparison
def today = LocalDate.of(2026, 3, 8)
def yesterday = LocalDate.of(2026, 3, 7)
def tomorrow = LocalDate.of(2026, 3, 9)

println "yesterday <=> today: ${yesterday <=> today}"     // -1
println "today <=> today: ${today <=> today}"              // 0
println "tomorrow <=> today: ${tomorrow <=> today}"        // 1

// Sort dates with spaceship
def dates = [
    LocalDate.of(2026, 12, 25),
    LocalDate.of(2026, 1, 1),
    LocalDate.of(2026, 7, 4),
    LocalDate.of(2026, 3, 8)
]

def sorted = dates.sort { a, b -> a <=> b }
println "\nSorted dates: ${sorted}"

// Reverse sort (newest first)
def reversed = dates.sort { a, b -> b <=> a }
println "Reverse dates: ${reversed}"

// LocalDateTime comparison
def morning = LocalDateTime.of(2026, 3, 8, 9, 0)
def evening = LocalDateTime.of(2026, 3, 8, 18, 0)
println "\nMorning <=> Evening: ${morning <=> evening}"    // -1

// Compare across years
def date2025 = LocalDate.of(2025, 12, 31)
def date2026 = LocalDate.of(2026, 1, 1)
println "2025-12-31 <=> 2026-01-01: ${date2025 <=> date2026}" // -1

Output

yesterday <=> today: -1
today <=> today: 0
tomorrow <=> today: 1

Sorted dates: [2026-01-01, 2026-03-08, 2026-07-04, 2026-12-25]
Reverse dates: [2026-12-25, 2026-07-04, 2026-03-08, 2026-01-01]

Morning <=> Evening: -1
2025-12-31 <=> 2026-01-01: -1

Date sorting with the spaceship operator is incredibly clean. Instead of a verbose Comparator.comparing() chain, you just use { a, b -> a <=> b } for ascending or { a, b -> b <=> a } for descending.

Example 4: Sorting Lists with the Spaceship Operator

This is where the spaceship operator really shines. Sorting closures become one-liners instead of multi-line Comparator implementations.

Example 4: Sorting with Spaceship

// Ascending sort (default with spaceship)
def numbers = [42, 17, 8, 99, 3, 56]
def ascending = numbers.sort { a, b -> a <=> b }
println "Ascending: ${ascending}"

// Descending sort (swap a and b)
def descending = numbers.sort(false) { a, b -> b <=> a }
println "Descending: ${descending}"

// Sort strings alphabetically
def fruits = ["banana", "apple", "cherry", "date", "elderberry"]
def sortedFruits = fruits.sort { a, b -> a <=> b }
println "\nFruits: ${sortedFruits}"

// Sort strings by length
def byLength = fruits.sort(false) { a, b -> a.length() <=> b.length() }
println "By length: ${byLength}"

// Sort by length descending, then alphabetically for same length
def complex = ["cat", "elephant", "dog", "ant", "bear", "fox"]
def sorted = complex.sort(false) { a, b ->
    def lenCompare = b.length() <=> a.length()  // descending length
    lenCompare != 0 ? lenCompare : a <=> b      // then alphabetical
}
println "\nComplex sort: ${sorted}"

// Sort with null-safe spaceship
def withNulls = [3, null, 1, null, 2]
def nullSafe = withNulls.sort { a, b ->
    if (a == null && b == null) return 0
    if (a == null) return 1    // nulls last
    if (b == null) return -1
    return a <=> b
}
println "Null-safe: ${nullSafe}"

Output

Ascending: [3, 8, 17, 42, 56, 99]
Descending: [99, 56, 42, 17, 8, 3]

Fruits: [apple, banana, cherry, date, elderberry]
By length: [date, apple, banana, cherry, elderberry]

Complex sort: [elephant, bear, ant, cat, dog, fox]
Null-safe: [1, 2, 3, null, null]

The trick for descending order is simple: swap the operands. Instead of a <=> b (ascending), write b <=> a (descending). For multi-level sorting, check the primary comparison first and fall through to secondary comparisons if the primary is equal (returns 0).

Example 5: Sorting Objects by Property

The most practical use: sorting a list of objects by one or more properties.

Example 5: Sorting Objects by Property

class Employee {
    String name
    String department
    int salary
    String toString() { "${name} (${department}, \$${salary})" }
}

def employees = [
    new Employee(name: "Charlie", department: "Engineering", salary: 95000),
    new Employee(name: "Alice", department: "Marketing", salary: 75000),
    new Employee(name: "Bob", department: "Engineering", salary: 105000),
    new Employee(name: "Diana", department: "Marketing", salary: 80000),
    new Employee(name: "Eve", department: "Engineering", salary: 95000)
]

// Sort by name
def byName = employees.sort(false) { a, b -> a.name <=> b.name }
println "By name:"
byName.each { println "  ${it}" }

// Sort by salary descending
def bySalary = employees.sort(false) { a, b -> b.salary <=> a.salary }
println "\nBy salary (highest first):"
bySalary.each { println "  ${it}" }

// Sort by department, then by salary descending within each department
def multiSort = employees.sort(false) { a, b ->
    def deptCompare = a.department <=> b.department
    deptCompare != 0 ? deptCompare : b.salary <=> a.salary
}
println "\nBy department, then salary desc:"
multiSort.each { println "  ${it}" }

Output

By name:
  Alice (Marketing, $75000)
  Bob (Engineering, $105000)
  Charlie (Engineering, $95000)
  Diana (Marketing, $80000)
  Eve (Engineering, $95000)

By salary (highest first):
  Bob (Engineering, $105000)
  Charlie (Engineering, $95000)
  Eve (Engineering, $95000)
  Diana (Marketing, $80000)
  Alice (Marketing, $75000)

By department, then salary desc:
  Bob (Engineering, $105000)
  Charlie (Engineering, $95000)
  Eve (Engineering, $95000)
  Diana (Marketing, $80000)
  Alice (Marketing, $75000)

Multi-level sorting with the spaceship operator follows a simple pattern: compare the primary field first. If the result is not zero (meaning they are different), return that result. If it is zero (meaning they are the same on the primary field), compare the secondary field.

Example 6: Implementing Comparable with the Spaceship Operator

Groovy makes it easy to implement the Comparable interface using the spaceship operator in your custom classes.

Example 6: Implementing Comparable

class Version implements Comparable<Version> {
    int major
    int minor
    int patch

    @Override
    int compareTo(Version other) {
        return (this.major <=> other.major) ?:
               (this.minor <=> other.minor) ?:
               (this.patch <=> other.patch)
    }

    String toString() { "${major}.${minor}.${patch}" }
}

def v1 = new Version(major: 1, minor: 0, patch: 0)
def v2 = new Version(major: 1, minor: 2, patch: 3)
def v3 = new Version(major: 2, minor: 0, patch: 0)
def v4 = new Version(major: 1, minor: 2, patch: 3)

// Direct comparison using spaceship
println "v1 <=> v2: ${v1 <=> v2}"     // -1
println "v2 <=> v3: ${v2 <=> v3}"     // -1
println "v2 <=> v4: ${v2 <=> v4}"     // 0
println "v3 <=> v1: ${v3 <=> v1}"     // 1

// Sorting works automatically
def versions = [v3, v1, v4, v2]
def sorted = versions.sort()
println "\nSorted: ${sorted}"

// Comparison operators also work thanks to Comparable
println "\nv1 < v2: ${v1 < v2}"       // true
println "v2 == v4: ${v2 == v4}"       // true
println "v3 > v1: ${v3 > v1}"         // true
println "v1 >= v1: ${v1 >= v1}"       // true

Output

v1 <=> v2: -1
v2 <=> v3: -1
v2 <=> v4: 0
v3 <=> v1: 1

Sorted: [1.0.0, 1.2.3, 1.2.3, 2.0.0]

v1 < v2: true
v2 == v4: true
v3 > v1: true
v1 >= v1: true

Notice the elegant (a <=> b) ?: (c <=> d) ?: (e <=> f) pattern. This chains the Elvis operator with the spaceship operator: if the first comparison returns 0 (which is falsy in Groovy), it falls through to the next comparison. This is the idiomatic Groovy way to implement multi-field comparison.

Example 7: Spaceship Operator with Collections — min, max, and Between

The spaceship operator underlies many collection operations. Understanding it helps you write custom min/max/clamp logic.

Example 7: Spaceship with min, max, clamp

// Custom min using spaceship
def minimum(a, b) { (a <=> b) <= 0 ? a : b }
def maximum(a, b) { (a <=> b) >= 0 ? a : b }

println "min(5, 3): ${minimum(5, 3)}"
println "max(5, 3): ${maximum(5, 3)}"
println "min('apple', 'banana'): ${minimum('apple', 'banana')}"

// Clamp a value between min and max
def clamp(value, min, max) {
    if ((value <=> min) < 0) return min
    if ((value <=> max) > 0) return max
    return value
}

println "\nClamp 5 to [1,10]: ${clamp(5, 1, 10)}"
println "Clamp -3 to [1,10]: ${clamp(-3, 1, 10)}"
println "Clamp 15 to [1,10]: ${clamp(15, 1, 10)}"

// Using spaceship in collection operations
def scores = [85, 92, 78, 95, 88, 72, 91]

// Find elements in a range using spaceship
def inRange = scores.findAll { (it <=> 80) >= 0 && (it <=> 90) <= 0 }
println "\nScores 80-90: ${inRange}"

// Sort and get top 3
def top3 = scores.sort(false) { a, b -> b <=> a }.take(3)
println "Top 3: ${top3}"

// Sort and get bottom 3
def bottom3 = scores.sort(false) { a, b -> a <=> b }.take(3)
println "Bottom 3: ${bottom3}"

Output

min(5, 3): 3
max(5, 3): 5
min('apple', 'banana'): apple

Clamp 5 to [1,10]: 5
Clamp -3 to [1,10]: 1
Clamp 15 to [1,10]: 10

Scores 80-90: [85, 88]
Top 3: [95, 92, 91]
Bottom 3: [72, 78, 85]

The clamp function is a practical utility that keeps a value within bounds. Combined with the spaceship operator, it reads almost like English: “if value is less than min, return min; if value is greater than max, return max; otherwise return value.”

Example 8: Spaceship with Elvis for Multi-Field Sorting

The spaceship operator combined with the Elvis operator creates an incredibly elegant multi-field sorting pattern.

Example 8: Multi-Field Sort with Spaceship + Elvis

class Student {
    String name
    String grade
    int score
    String toString() { "${name}: ${grade} (${score})" }
}

def students = [
    new Student(name: "Alice", grade: "A", score: 95),
    new Student(name: "Bob", grade: "B", score: 85),
    new Student(name: "Charlie", grade: "A", score: 92),
    new Student(name: "Diana", grade: "B", score: 88),
    new Student(name: "Eve", grade: "A", score: 95),
    new Student(name: "Frank", grade: "C", score: 75)
]

// Sort by grade ascending, then score descending, then name ascending
def sorted = students.sort(false) { a, b ->
    (a.grade <=> b.grade) ?:        // Primary: grade A-Z
    (b.score <=> a.score) ?:        // Secondary: score high-to-low
    (a.name <=> b.name)             // Tertiary: name A-Z
}

println "Multi-field sort:"
sorted.each { println "  ${it}" }

// The pattern: (field1 <=> field1) ?: (field2 <=> field2) ?: ...
// Each ?: says "if previous comparison was equal (0), try this one"
// Swap a/b to reverse that field's direction

// Same logic with explicit explanation
println "\nExplanation of sort order:"
students.sort(false) { a, b ->
    def result = a.grade <=> b.grade     // First by grade
    if (result != 0) return result
    result = b.score <=> a.score         // Then by score (descending)
    if (result != 0) return result
    return a.name <=> b.name             // Then by name
}.each { println "  ${it}" }

Output

Multi-field sort:
  Alice: A (95)
  Eve: A (95)
  Charlie: A (92)
  Diana: B (88)
  Bob: B (85)
  Frank: C (75)

Explanation of sort order:
  Alice: A (95)
  Eve: A (95)
  Charlie: A (92)
  Diana: B (88)
  Bob: B (85)
  Frank: C (75)

The (a.field <=> b.field) ?: (a.field2 <=> b.field2) pattern is the crown jewel of Groovy sorting. It is short, readable, and handles any number of sort levels. The Elvis operator works here because 0 (equal) is falsy in Groovy, so it falls through to the next comparison.

Example 9: Spaceship Operator with Enums

Enums in Groovy implement Comparable by their declaration order, making the spaceship operator work naturally.

Example 9: Spaceship with Enums

enum Priority {
    LOW, MEDIUM, HIGH, CRITICAL
}

// Enums compare by declaration order
println "LOW <=> HIGH: ${Priority.LOW <=> Priority.HIGH}"          // -1
println "HIGH <=> LOW: ${Priority.HIGH <=> Priority.LOW}"          // 1
println "HIGH <=> HIGH: ${Priority.HIGH <=> Priority.HIGH}"        // 0
println "CRITICAL <=> LOW: ${Priority.CRITICAL <=> Priority.LOW}"  // 1

// Sort tasks by priority
def tasks = [
    [title: "Fix bug", priority: Priority.HIGH],
    [title: "Write docs", priority: Priority.LOW],
    [title: "Security patch", priority: Priority.CRITICAL],
    [title: "Add feature", priority: Priority.MEDIUM],
    [title: "Update deps", priority: Priority.LOW]
]

// Sort by priority (highest first)
def byPriority = tasks.sort(false) { a, b -> b.priority <=> a.priority }
println "\nTasks by priority (highest first):"
byPriority.each { println "  [${it.priority}] ${it.title}" }

// Sort by priority then alphabetically
def sortedTasks = tasks.sort(false) { a, b ->
    (b.priority <=> a.priority) ?: (a.title <=> b.title)
}
println "\nTasks sorted (priority then name):"
sortedTasks.each { println "  [${it.priority}] ${it.title}" }

Output

LOW <=> HIGH: -1
HIGH <=> LOW: 1
HIGH <=> HIGH: 0
CRITICAL <=> LOW: 1

Tasks by priority (highest first):
  [CRITICAL] Security patch
  [HIGH] Fix bug
  [MEDIUM] Add feature
  [LOW] Write docs
  [LOW] Update deps

Tasks sorted (priority then name):
  [CRITICAL] Security patch
  [HIGH] Fix bug
  [MEDIUM] Add feature
  [LOW] Update deps
  [LOW] Write docs

Enum comparison is ordered by the declaration sequence. So if you want CRITICAL to sort as highest, declare it last in the enum. Then b.priority <=> a.priority will sort from highest to lowest.

Example 10: Spaceship Operator with Null Handling

The spaceship operator handles null on one or both sides differently than you might expect. Let us explore the behavior.

Example 10: Spaceship with Null Handling

// Null comparisons in Groovy
println "null <=> null: ${null <=> null}"     // 0
println "null <=> 5: ${null <=> 5}"           // -1
println "5 <=> null: ${5 <=> null}"           // 1
println "null <=> 'a': ${null <=> 'a'}"       // -1

// Groovy treats null as "less than" any non-null value
// This means nulls sort to the beginning by default

// Sorting with nulls
def numbers = [3, null, 1, null, 5, 2]
def sorted = numbers.sort { a, b -> a <=> b }
println "\nSorted (nulls first): ${sorted}"

// To sort nulls to the end
def nullsLast = numbers.sort(false) { a, b ->
    if (a == null && b == null) return 0
    if (a == null) return 1     // null goes after non-null
    if (b == null) return -1
    return a <=> b
}
println "Sorted (nulls last): ${nullsLast}"

// Null-safe utility for sorting
def nullSafeCompare(a, b, boolean nullsFirst = true) {
    if (a == null && b == null) return 0
    if (a == null) return nullsFirst ? -1 : 1
    if (b == null) return nullsFirst ? 1 : -1
    return a <=> b
}

def names = ["Charlie", null, "Alice", null, "Bob"]
def sorted1 = names.sort(false) { a, b -> nullSafeCompare(a, b, true) }
def sorted2 = names.sort(false) { a, b -> nullSafeCompare(a, b, false) }
println "\nNames (nulls first): ${sorted1}"
println "Names (nulls last): ${sorted2}"

Output

null <=> null: 0
null <=> 5: -1
5 <=> null: 1
null <=> 'a': -1

Sorted (nulls first): [null, null, 1, 2, 3, 5]
Sorted (nulls last): [1, 2, 3, 5, null, null]

Names (nulls first): [null, null, Alice, Bob, Charlie]
Names (nulls last): [Alice, Bob, Charlie, null, null]

Groovy’s spaceship operator handles null gracefully: null is treated as “less than” everything, and two nulls are equal. This is actually helpful because it means sorting with nulls does not crash. If you want nulls at the end instead of the beginning, you need a small wrapper.

Example 11: Spaceship Operator for Custom Sorting Logic

Beyond simple property comparisons, the spaceship operator can be used in creative ways for domain-specific sorting.

Example 11: Custom Sorting Logic

// Sort strings by natural number order (not lexicographic)
def files = ["file10.txt", "file2.txt", "file1.txt", "file20.txt", "file3.txt"]

// Lexicographic sort (wrong for numbers)
def lexSort = files.sort(false)
println "Lex sort: ${lexSort}"

// Natural sort using spaceship with extracted numbers
def naturalSort = files.sort(false) { a, b ->
    def numA = (a =~ /\d+/)[0] as int
    def numB = (b =~ /\d+/)[0] as int
    numA <=> numB
}
println "Natural sort: ${naturalSort}"

// Sort IP addresses numerically
def ips = ["192.168.1.10", "10.0.0.1", "192.168.1.2", "10.0.0.100"]
def sortedIPs = ips.sort(false) { a, b ->
    def partsA = a.split('\\.').collect { it as int }
    def partsB = b.split('\\.').collect { it as int }
    (partsA[0] <=> partsB[0]) ?:
    (partsA[1] <=> partsB[1]) ?:
    (partsA[2] <=> partsB[2]) ?:
    (partsA[3] <=> partsB[3])
}
println "\nSorted IPs: ${sortedIPs}"

// Sort mixed data with custom priority
def items = ["ERROR: disk full", "INFO: started", "WARN: low memory",
             "ERROR: timeout", "INFO: stopped", "WARN: slow query"]

def levelOrder = [ERROR: 0, WARN: 1, INFO: 2]
def sortedLogs = items.sort(false) { a, b ->
    def levelA = levelOrder[a.split(':')[0]]
    def levelB = levelOrder[b.split(':')[0]]
    (levelA <=> levelB) ?: (a <=> b)
}
println "\nSorted logs:"
sortedLogs.each { println "  ${it}" }

Output

Lex sort: [file1.txt, file10.txt, file2.txt, file20.txt, file3.txt]
Natural sort: [file1.txt, file2.txt, file3.txt, file10.txt, file20.txt]

Sorted IPs: [10.0.0.1, 10.0.0.100, 192.168.1.2, 192.168.1.10]

Sorted logs:
  ERROR: disk full
  ERROR: timeout
  WARN: low memory
  WARN: slow query
  INFO: started
  INFO: stopped

The natural file sort and IP address sort examples show the real power of the spaceship operator. Extract the comparable parts, then use <=> with ?: chaining for multi-field comparison. The result is clean, readable sorting logic that would take dozens of lines in Java.

Spaceship Operator for Sorting

Let us summarize the key sorting patterns with the spaceship operator in one reference.

Spaceship Sorting Patterns Reference

def data = [[name: "Charlie", age: 30], [name: "Alice", age: 25],
            [name: "Bob", age: 30], [name: "Diana", age: 25]]

// Pattern 1: Ascending by single field
def asc = data.sort(false) { a, b -> a.name <=> b.name }
println "Ascending name: ${asc*.name}"

// Pattern 2: Descending by single field (swap a and b)
def desc = data.sort(false) { a, b -> b.age <=> a.age }
println "Descending age: ${desc.collect { "${it.name}(${it.age})" }}"

// Pattern 3: Multi-field with spaceship + elvis
def multi = data.sort(false) { a, b ->
    (a.age <=> b.age) ?: (a.name <=> b.name)
}
println "Age then name: ${multi.collect { "${it.name}(${it.age})" }}"

// Pattern 4: Shorthand with sort { it.property }
def shorthand = data.sort(false) { it.name }
println "Shorthand: ${shorthand*.name}"

// Pattern 5: Using sort with Closure returning Comparable
// This is equivalent to Pattern 1 but shorter
def byAge = data.sort(false) { it.age }
println "By age: ${byAge.collect { "${it.name}(${it.age})" }}"

Output

Ascending name: [Alice, Bob, Charlie, Diana]
Descending age: [Charlie(30), Bob(30), Alice(25), Diana(25)]
Age then name: [Alice(25), Diana(25), Bob(30), Charlie(30)]
Shorthand: [Alice, Bob, Charlie, Diana]
By age: [Alice(25), Diana(25), Charlie(30), Bob(30)]

For single-field ascending sorts, Groovy’s sort { it.property } shorthand is even cleaner than the spaceship. But for descending sorts and multi-field sorts, the spaceship operator is essential.

Spaceship Operator vs compareTo()

Spaceship vs compareTo()

// They produce the same result
def a = "apple"
def b = "banana"

println "compareTo: ${a.compareTo(b)}"
println "spaceship: ${a <=> b}"
assert a.compareTo(b) == (a <=> b)

// Spaceship advantages:
// 1. More readable in sorting closures
def list1 = [3, 1, 2].sort { x, y -> x.compareTo(y) }  // verbose
def list2 = [3, 1, 2].sort { x, y -> x <=> y }         // concise
assert list1 == list2

// 2. Handles null (compareTo throws NPE on null receiver)
println "\nspaceship null: ${null <=> 5}"   // Works: -1
// println "compareTo null: ${null.compareTo(5)}"  // NPE!

// 3. Better for multi-field chaining with Elvis
// compareTo:
// int result = a.field1.compareTo(b.field1)
// if (result == 0) result = a.field2.compareTo(b.field2)
// if (result == 0) result = a.field3.compareTo(b.field3)

// spaceship + elvis:
// (a.field1 <=> b.field1) ?: (a.field2 <=> b.field2) ?: (a.field3 <=> b.field3)

println "\nWinner: spaceship operator for Groovy code!"

Output

compareTo: -1
spaceship: -1

spaceship null: -1

Winner: spaceship operator for Groovy code!

Best Practice: Always use the spaceship operator instead of compareTo() in Groovy code. It is more concise, handles null safely, and chains beautifully with the Elvis operator for multi-field comparisons.

Real-World Use Cases

Use Case 1: Sorting Search Results by Relevance

Search results often need multi-criteria sorting: relevance first, then date, then alphabetical.

Real-World: Sorting Search Results

import java.time.LocalDate

def results = [
    [title: "Groovy Operators Guide", relevance: 95, date: LocalDate.of(2026, 3, 1), views: 1200],
    [title: "Groovy Basics Tutorial", relevance: 80, date: LocalDate.of(2026, 2, 15), views: 3500],
    [title: "Groovy Spaceship Deep Dive", relevance: 95, date: LocalDate.of(2026, 3, 5), views: 800],
    [title: "Groovy Collections Guide", relevance: 85, date: LocalDate.of(2026, 1, 20), views: 2200],
    [title: "Groovy for Java Developers", relevance: 80, date: LocalDate.of(2026, 2, 28), views: 5000]
]

// Sort by: relevance desc, date desc, views desc
def sorted = results.sort(false) { a, b ->
    (b.relevance <=> a.relevance) ?:   // Highest relevance first
    (b.date <=> a.date) ?:             // Newest first
    (b.views <=> a.views)              // Most viewed first
}

println "Search results:"
sorted.each {
    println "  [${it.relevance}] ${it.title} (${it.date}, ${it.views} views)"
}

// Alternative: sort by views for "Popular" tab
def popular = results.sort(false) { a, b -> b.views <=> a.views }
println "\nPopular:"
popular.each { println "  ${it.title} (${it.views} views)" }

Output

Search results:
  [95] Groovy Spaceship Deep Dive (2026-03-05, 800 views)
  [95] Groovy Operators Guide (2026-03-01, 1200 views)
  [85] Groovy Collections Guide (2026-01-20, 2200 views)
  [80] Groovy for Java Developers (2026-02-28, 5000 views)
  [80] Groovy Basics Tutorial (2026-02-15, 3500 views)

Popular:
  Groovy for Java Developers (5000 views)
  Groovy Basics Tutorial (3500 views)
  Groovy Collections Guide (2200 views)
  Groovy Operators Guide (1200 views)
  Groovy Spaceship Deep Dive (800 views)

Use Case 2: Leaderboard Ranking

Games and competitions need leaderboard sorting with tiebreakers. The spaceship operator handles this elegantly.

Real-World: Leaderboard Ranking

def players = [
    [name: "Alice", score: 2500, time: 45, level: 12],
    [name: "Bob", score: 2500, time: 52, level: 12],
    [name: "Charlie", score: 3100, time: 60, level: 15],
    [name: "Diana", score: 2500, time: 45, level: 14],
    [name: "Eve", score: 1800, time: 30, level: 8]
]

// Leaderboard: highest score, then lowest time, then highest level, then name
def leaderboard = players.sort(false) { a, b ->
    (b.score <=> a.score) ?:       // Highest score first
    (a.time <=> b.time) ?:         // Lowest time first (faster is better)
    (b.level <=> a.level) ?:       // Highest level first
    (a.name <=> b.name)            // Alphabetical tiebreaker
}

println "Leaderboard:"
leaderboard.eachWithIndex { player, idx ->
    println "  #${idx + 1} ${player.name}: ${player.score} pts, ${player.time}s, level ${player.level}"
}

// Calculate rankings with ties
def rank = 1
def prevScore = null
leaderboard.eachWithIndex { player, idx ->
    if (player.score != prevScore) rank = idx + 1
    println "  Rank ${rank}: ${player.name} (${player.score})"
    prevScore = player.score
}

Output

Leaderboard:
  #1 Charlie: 3100 pts, 60s, level 15
  #2 Diana: 2500 pts, 45s, level 14
  #3 Alice: 2500 pts, 45s, level 12
  #4 Bob: 2500 pts, 52s, level 12
  #5 Eve: 1800 pts, 30s, level 8
  Rank 1: Charlie (3100)
  Rank 2: Diana (2500)
  Rank 2: Alice (2500)
  Rank 2: Bob (2500)
  Rank 5: Eve (1800)

This leaderboard example demonstrates four levels of sorting criteria in a single closure. Without the spaceship operator, this would be a complex, multi-line Comparator implementation. With it, the entire sorting logic fits in four readable lines.

Edge Cases and Best Practices

Important Edge Cases

  • Null handling: The spaceship operator treats null as less than any non-null value. null <=> null returns 0.
  • Type mismatch: Comparing incompatible types (like "hello" <=> 42) throws a ClassCastException. Ensure both sides are compatible.
  • Zero is falsy: When chaining <=> with ?:, remember that 0 (equal) is falsy, which is exactly what makes the pattern work.
  • sort() mutates: list.sort() mutates the original list. Use list.sort(false) to get a new sorted list without modifying the original.
  • Consistent ordering: If your comparator does not handle all cases consistently (transitivity), you will get unpredictable sort results.

Best Practices Summary

  • Use <=> instead of compareTo() in Groovy code for readability
  • Swap operands (b <=> a) for descending order
  • Chain with Elvis: (a.x <=> b.x) ?: (a.y <=> b.y) for multi-field sorts
  • Use sort(false) to avoid mutating the original list
  • For single-field ascending sorts, the shorthand sort { it.field } is even cleaner

Conclusion

The Groovy spaceship operator (<=>) transforms comparison and sorting from a verbose, boilerplate-heavy task into a concise, expressive operation. It handles sorting of numbers, strings, dates, and custom objects with multi-field tiebreakers – all with minimal code.

The real magic happens when you combine it with the Elvis operator for multi-field sorting: (a.field1 <=> b.field1) ?: (a.field2 <=> b.field2). This pattern is one of the most elegant idioms in all of Groovy.

Summary

  • a <=> b returns -1, 0, or 1 (less than, equal, greater than)
  • It delegates to compareTo() but handles null safely
  • Swap operands for descending: b <=> a
  • Chain with Elvis for multi-field sorting: (x <=> y) ?: (p <=> q)
  • Works with numbers, strings, dates, enums, and any Comparable type
  • Use sort(false) to get a new sorted list without mutating the original

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 Regular Expressions – Pattern Matching

Frequently Asked Questions

What is the spaceship operator in Groovy?

The spaceship operator (<=>) in Groovy performs three-way comparison between two values. It returns -1 if the left value is less than the right, 0 if they are equal, and 1 if the left is greater. It delegates to the compareTo() method and works with any Comparable type including numbers, strings, dates, and custom objects.

How do I sort in descending order using the Groovy spaceship operator?

To sort in descending order, swap the operands. For ascending: list.sort { a, b -> a <=> b }. For descending: list.sort { a, b -> b <=> a }. The swap tells the sort algorithm to reverse the natural order. This works for any type — numbers, strings, dates, or custom objects.

How do I sort by multiple fields using the spaceship operator in Groovy?

Chain spaceship comparisons with the Elvis operator: (a.field1 <=> b.field1) ?: (a.field2 <=> b.field2) ?: (a.field3 <=> b.field3). If the first comparison returns 0 (equal, which is falsy), the Elvis operator falls through to the next comparison. Swap a and b for any field you want in descending order.

Does the Groovy spaceship operator handle null values?

Yes. The spaceship operator handles null safely: null is treated as less than any non-null value, and null <=> null returns 0. This means nulls sort to the beginning by default. To sort nulls to the end, add explicit null checks in your comparator before using the spaceship operator.

What is the difference between the spaceship operator and compareTo() in Groovy?

The spaceship operator (<=>) is syntactic sugar for compareTo() with two key advantages: it handles null safely (compareTo throws NPE on null), and it chains elegantly with the Elvis operator for multi-field sorting. Use the spaceship operator in all Groovy code for better readability and null safety.

Previous in Series: Groovy Spread Operator (*.) – Apply to All Elements

Next in Series: Groovy Regular Expressions – Pattern Matching

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 *