Operators: Arithmetic, Relational & Logical

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Groovy 6.0.0 (released 2026-09-22) running on JDK 26, and every example on it was run with that combination. Groovy keeps all of Java’s operators and adds a few of its own. The additions that matter most on this page are the power operator **, the Elvis operator ?: and its assignment form ?=, and logical implication ==> (Groovy 5.0). Operators on objects (?., .@, .&, *., ranges, subscripts, in, as) are covered in Object Operators, the regular-expression operators in Regular Expressions, and how a class of your own can take part in all of them in Operator Overloading.

The running scenario is a small greenhouse: trays, water, zones and pump flags.

Arithmetic operators

The binary arithmetic operators are +, -, *, /, % and ** (power). Two behaviours differ from Java and are the usual source of surprises:

  • / never truncates. Dividing two integers gives a BigDecimal (here 7 / 2 is 3.5). Use intdiv() when you want integer division; it truncates toward zero.

  • ** returns the narrowest type that holds the result: an Integer while the value fits, a BigInteger when it does not, and a Double for a negative exponent.

int trays = 14
int seedsPerTray = 6
assert trays * seedsPerTray == 84
assert 7 / 2 == 3.5          // / on integers gives a BigDecimal
assert 7.intdiv(2) == 3      // integer division
assert 7 % 3 == 1
assert -7 % 3 == -1          // remainder keeps the sign of the dividend
assert (-7).mod(3) == 2      // mod is always non-negative
assert 2 ** 10 == 1024
assert 2 ** -1 == 0.5
assert (2 ** 3).class == Integer
assert (2 ** 100).class == BigInteger
println "7/2 -> ${7 / 2} (${(7 / 2).class.simpleName})"
println "2**-1 -> ${2 ** -1} (${(2 ** -1).class.simpleName})"
println "-7 % 3 = ${-7 % 3}, (-7).mod(3) = ${(-7).mod(3)}"
println "2**100 = ${2 ** 100}"
7/2 -> 3.5 (BigDecimal)
2**-1 -> 0.5 (Double)
-7 % 3 = -1, (-7).mod(3) = 2
2**100 = 1267650600228229401496703205376

The result type of a mixed expression follows the numeric promotion rules covered in Numbers & Numeric Literals. The points worth remembering here: int arithmetic wraps on overflow exactly as in Java, a long operand makes the result a long, and a decimal literal such as 0.1 is a BigDecimal, so 0.1 + 0.2 == 0.3 holds, while with double operands it does not.

assert [1, 2, 3, 2] - 2 == [1, 3]
assert [1, 2] * 2 == [1, 2, 1, 2]
assert 'ab' * 2 == 'abab'
assert 'a' + 1 == 'a1'
assert (1 + 1.5).class == BigDecimal
assert (1 + 1.5d).class == Double
assert (1 + 2L).class == Long
assert (Integer.MAX_VALUE + 1) == -2147483648       // int arithmetic wraps, as in Java
assert (Integer.MAX_VALUE + 1L) == 2147483648L
assert 0.1 + 0.2 == 0.3                              // BigDecimal literals
assert 0.1d + 0.2d != 0.3d                           // double literals
assert 7.intdiv(2) == 3 && (-7).intdiv(2) == -3      // truncates toward zero
println(1 / 3)
println((1 / 3).class.simpleName)
0.3333333333
BigDecimal

The same operators work on strings and collections because the GDK defines plus, minus and multiply for them (the first four assertions above). That is ordinary operator overloading, explained on the Operator Overloading page.

Unary operators and increment

+ and - are also unary, and ++ and -- exist in both prefix and postfix form. As in Java, the postfix form yields the old value and the prefix form the new one. Unary minus applies to what follows it: use parentheses to negate an expression.

def a = 2
def b = a++ * 3
assert a == 3 && b == 6          // postfix: use the old value, then increment
def e = 1
def f = ++e + 3
assert e == 2 && f == 5          // prefix: increment, then use the new value
assert -(-1) == 1
assert +3 == 3
println "a = $a, b = $b, e = $e, f = $f"
a = 3, b = 6, e = 2, f = 5

On objects, ++ and -- call next() and previous(), which is what lets them step through ranges and your own types; see Operator Overloading.

Assignment arithmetic operators

Every binary arithmetic operator has an assignment form: +=, -=, *=, /=, %= and **=. For a plain value, x op= y is shorthand for x = x op y, so the type of the variable matters. With def the variable simply takes the new type; with a declared type the result is cast back, which silently truncates:

int water = 100
water -= 15; water *= 2; water /= 4; water %= 20; water **= 2
println "water = $water (${water.class.simpleName})"
def log = []
log += 'dawn'        // list += element: a new list is bound to log
log << 'noon'        // leftShift: appends in place
println log
def s = 'ab'
s *= 3
println s
water = 4 (Integer)
[dawn, noon]
ababab

Here water /= 4 produces 42.5 (a BigDecimal), which is cast back to the int variable as 42; that is why the final value is 4 and not 6.25. Declare the variable with def (or a BigDecimal type) if you need the fraction.

Since Groovy 6.0, a class can define dedicated plusAssign-style methods so that += mutates the receiver instead of rebinding the variable. This is covered in Operator Overloading.

Relational operators

The relational operators are ==, !=, <, <=, >, >=, and, since Groovy 3.0, the identity operators === and !==. They behave differently from Java in one important way: == is value equality, not reference equality.

  • a == b calls equals (null-safe). If the class implements Comparable, == uses compareTo instead, so a Comparable class without equals still compares by value. Numbers are compared by value across types, so 1 == 1.0 is true.

  • <, <=, > and >= call compareTo, and null is considered less than any non-null value.

  • === and !== are shorthand for a.is(b) and its negation, that is, Java’s == on references.

  • The spaceship operator <=> returns the result of compareTo (see Object Operators).

import groovy.transform.EqualsAndHashCode
@EqualsAndHashCode
class Zone { String name }
def a = new Zone(name: 'north')
def alias = a
def b = new Zone(name: 'north')
assert a == b && a.equals(b)
assert a === alias && a.is(alias)
assert a !== b
assert 1 == 1.0           // numbers are compared by value across types
assert 'abc' == "abc"
assert [1, 2, 3] == [1, 2, 3]
assert null == null && null != 0
assert 'apple' < 'banana'
assert 5 <=> 3 == 1
assert 3 <=> 3 == 0
assert 1G == 1 && 1.0 == 1.00G
println 'ok'
println "boxed identity: ${Integer.valueOf(1000) === Integer.valueOf(1000)}"
def x = null
println (x == 'a')
try { println (null < 1) } catch (e) { println e.class.simpleName }
try { println ([1] < [2]) } catch (e) { println e.class.simpleName + ': ' + e.message.readLines()[0] }
ok
boxed identity: false
false
true
IllegalArgumentException: Cannot compare java.util.ArrayList with value '[1]' and java.util.ArrayList with value '[2]'

Lists are comparable for equality but not for ordering, so [1] < [2] fails at runtime. Two practical consequences: use === only when you really mean "the same object", and never use it to compare boxed numbers or strings, whose identity depends on caching. The Java habit of calling equals for value comparison is unnecessary; == already does it and is null-safe.

Logical operators

Groovy has &&, || and !, with the usual precedence: ! binds tighter than &&, which binds tighter than ||. Operands are tested with the Groovy truth (see Booleans & the Groovy Truth), so a non-empty string, a non-zero number or a non-empty collection counts as true. Unlike JavaScript or Python, the result is always a boolean, never one of the operands. Both && and || short-circuit: the right operand is evaluated only when the left one does not already decide the result.

Groovy 5.0 added logical implication, a ==> b, defined as !a || b. It is true unless a is true and b is false, and it short-circuits like ||. It is not an overloadable operator; the compiler expands it, so a class that defines an implies method does not change what ==> does (the Boolean GDK method implies exists for method-call syntax).

import groovy.transform.Field

@Field int calls = 0
boolean probe(boolean v) { calls++; v }

assert (false && probe(true)) == false && calls == 0   // right side skipped
assert (true || probe(false)) && calls == 0            // right side skipped
assert (true && probe(true)) && calls == 1             // right side evaluated
// the operands are tested with the Groovy truth, and the result is a boolean
assert ('x' && 'y') === true
assert ('' || 'y') === true
assert ('' || 0) === false
// the guard idiom
def zone = null
assert !(zone && zone.name)
// implication (5.0): a ==> b is !a || b
assert false ==> false
assert false ==> true
assert true ==> true
assert !(true ==> false)
int sensors = 0
def read = { sensors++; true }
assert (false ==> read())   // right side short-circuited
assert sensors == 0
// precedence
assert (!false && false) == false
assert true || true && false
println "calls = $calls, sensors = $sensors"
calls = 1, sensors = 0

Bitwise and shift operators

The bitwise operators are &, |, ^ (xor) and ~ (negation); the shift operators are <<, >> and >>> (unsigned). The bitwise operators apply to byte, short, int, long and BigInteger; the result is a BigInteger if either operand is one, a long if either is a long, and an int otherwise. >>> does not apply to BigInteger. All of them are overloadable, which is why << also appends to a list.

Primitive integers are signed, so apply a mask after ~ to see only the bits you care about. The compound forms &=, |=, ^=, <<=, >>= and >>>= also exist.

int pumps = 0b0101        // pump 0 and pump 2 are on
int PUMP_1 = 0b0010
pumps |= PUMP_1
assert pumps == 0b0111
pumps &= ~PUMP_1
assert pumps == 0b0101
pumps ^= 0b0001
assert pumps == 0b0100
assert (pumps & 0b0100) != 0
assert ((~5) & 0xFF) == 250
assert 1 << 4 == 16
assert -200 >> 20 == -1
assert -200 >>> 20 == 4095
assert (1 << 31) == Integer.MIN_VALUE
assert (3G << 4) == 48G
println "pumps = ${Integer.toBinaryString(pumps)}"
pumps = 100

Not, ternary, Elvis and Elvis assignment

The not operator ! negates a Groovy-truth test, so !'' is true and !'foo' is false.

The ternary operator cond ? a : b is an expression-sized if/else; because the condition uses the Groovy truth, name ? name : 'n/a' needs no explicit comparison.

The Elvis operator ?: shortens it further: x ?: y means x if x is truthy, otherwise y, with x evaluated only once. Note that "truthy" is not "non-null": 0, an empty string and an empty collection all fall through to the default, which makes ?: the wrong tool when 0 is a legitimate value.

The Elvis assignment operator ?= (Groovy 3.0) assigns only when the left side is falsy: x ?= y is the same as x = x ?: y. It works on variables, properties and map keys.

int pumps = 0b0100
def label = pumps ? 'running' : 'idle'
assert label == 'running'
def note = null
assert (note ?: 'none') == 'none'
assert (0 ?: 'zero is falsy') == 'zero is falsy'
assert (0.0 ?: 'x') == 'x'
assert !'' && !null && !0 && !'x'.isEmpty()
class Bed { String crop; int days }
def bed = new Bed(crop: null, days: 0)
bed.crop ?= 'basil'
bed.days ?= 30
bed.crop ?= 'mint'           // already set: unchanged
assert bed.crop == 'basil' && bed.days == 30
def m = [:]
m.target ?= 21
m.target ?= 25
assert m.target == 21
println "bed = ${bed.crop}/${bed.days}"
def t = 25
println (t > 30 ? 'hot' : t > 18 ? 'mild' : 'cold')
bed = basil/30
mild

The last line shows a nested ternary: ? is right-associative, so the chain reads left to right. Prefer a switch when there are more than two or three branches (see Control Structures). Safe navigation ?., which is often combined with ?:, is described in Object Operators.

Operator precedence

The table lists every Groovy operator from the tightest binding (level 1) to the loosest (level 15). Within one level the order is left to right, except for assignment.

Level Operators Meaning

1

new, (), {}, [], ., .&, .@, ?., *, *., *:, ~, !, (type), [], ?[], ++ and -- (postfix)

object creation, parentheses, method call, closure, list and map literals, member access, method pointer, field access, safe navigation, spread operators, bitwise negate or pattern, not, cast, index and safe index, post increment and decrement

2

**

power

3

++, --, +, - (prefix and unary)

pre increment and decrement, unary plus and minus

4

*, /, %

multiply, divide, remainder

5

+, -

addition, subtraction

6

<<, >>, >>>, .., ..<, <..<, <..

shifts and ranges

7

<, <=, >, >=, in, !in, instanceof, !instanceof, as

ordering, membership, type tests, coercion

8

==, !=, <=>, ===, !==

equality, spaceship, identity

8

=~, ==~

regex find and match

9

&

bitwise and

10

^

bitwise xor

11

|

bitwise or

12

&&

logical and

13

||

logical or

13.5

==>

logical implication (5.0)

14

? :, ?:

ternary, Elvis

15

=, **=, *=, /=, %=, +=, -=, <<=, >>=, >>>=, &=, ^=, |=, ?=

assignment forms

A few consequences, each checked by running it:

println((1 | 2) == 3)    // parentheses needed: == binds tighter than |
println 2 ** 3 ** 2      // ** groups left to right
println(-2 ** 2)         // ** binds tighter than unary minus
println((-2) ** 2)
println(1 + 2 << 1)      // + before <<
println(1 << 2 + 1)
println(true || false ? 'a' : 'b')
true
64
-4
4
6
8
a

Two of these are traps. Writing 1 | 2 == 3 without parentheses does not compile to what it looks like: == is level 8, | is level 11, so Groovy evaluates 2 == 3 first and then fails at run time with a MissingMethodException for Integer.or(Boolean). And 2 ** 3 ** 2 is 64, not 512: ** groups left to right here, unlike the mathematical convention. When in doubt, add parentheses; they cost nothing and they document the intent.

Where the book is outdated

Books on Groovy written before 3.0 describe a smaller operator set. Compared with them, the current language adds the identity operators === and !== (3.0), the Elvis assignment ?= (3.0), ==> (5.0) and, on the overloading side, the *Assign methods (6.0), and % dispatches to remainder in Groovy 6.0.0 (see Operator Overloading). The behaviours older books warn about, / returning a BigDecimal, == meaning equals, and the Groovy truth in conditions, are unchanged.