Differences with Java

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.

Groovy tries to follow the principle of least surprise for Java developers: most Java source is also valid Groovy, and the compiled classes are ordinary JVM classes that Java code can call and extend. This page lists the places where the two languages disagree, one section per row of the official Differences with Java page, each with a Groovy example that was run for real. The baseline is Groovy 6.0.0 (released 2026-09-22, JDK 17 or newer), and every example was executed with Groovy 6.0.0 on JDK 26 on 2026-10-14. Where a difference only exists in an older release, the release is named.

The examples use the same running idea as the rest of the section, a greenhouse with sensors, only where it helps. The Java side of each topic is documented in the Java section, linked as "the Java side" below: primitive types, nested classes, lambdas, strings and operators.

Valid Java is (almost) valid Groovy

Groovy was designed so that a Java developer can paste Java into a .groovy file and, most of the time, get the same program. The sections below are the exceptions, and they fall into three families.

Family What differs Sections

Same syntax, different behaviour

The code compiles in both languages but does something else at run time.

Multi-methods, ==, primitives and widening versus boxing, positive and negative zero, GString arguments to Object parameters

Java syntax Groovy reads differently

The code is rejected or means something else in Groovy.

Array initializers ({ ... } is a closure), omitted modifiers (a property, not a package-private field), $ inside double-quoted strings, single-character literals

Java syntax Groovy added later

Missing in old Groovy, available from the release named.

Try-with-resources (3.0), y.new X() (3.0), lambdas and :: (3.0), === (3.0), new int[] { ... } (3.0), java.time default import (5.0)

The first family is the dangerous one, because nothing fails: the program just gives a different answer.

Default imports

Groovy imports a larger set of packages than Java’s single java.lang. You can use these without an import statement (java.time.* was added in Groovy 5.0):

  • java.io.*

  • java.lang.*

  • java.math.BigDecimal and java.math.BigInteger

  • java.net.*

  • java.time.*

  • java.util.*

  • groovy.lang.*

  • groovy.util.*

// no import statements at all
def today = LocalDate.of(2026, 10, 14)                      // java.time.*
def total = new BigDecimal('10.50') + new BigInteger('3')   // java.math.BigDecimal and BigInteger
def list = new ArrayList<String>(['b', 'a'])                // java.util.*
def file = new File('x.txt')                                // java.io.*
def url = new URI('http://localhost:8080/x')                // java.net.*
def expando = new Expando(name: 'tomato')                   // groovy.util.*
def range = new IntRange(true, 1, 3)                        // groovy.lang.*
println "$today $total $list ${file.name} ${url.port} $expando.name $range"
assert Math.max(1, 2) == 2                                  // java.lang.*
2026-10-14 13.50 [b, a] x.txt 8080 tomato 1..3

Everything outside that list still needs an import, for example java.util.concurrent, java.nio.file and java.util.function, and so does groovy.transform (where @CompileStatic and @PackageScope live). A missing import is a compile-time error:

imports2.groovy: 2: unable to resolve class CountDownLatch
 @ line 2, column 5.
       new CountDownLatch(1)

Source: Groovy documentation: Differences with Java, default imports. The mechanics of imports (aliases, static imports, star imports) are on Program Structure, Packages, Imports and Scripts.

Multi-methods: runtime dispatch

Java chooses which overloaded method to call at compile time, from the declared types of the arguments. Groovy chooses at run time, from the actual types. Groovy calls this runtime dispatch or multi-methods. The same source, valid in both languages, gives different answers:

int method(String arg) { 1 }
int method(Object arg) { 2 }

Object o = 'Object'
int result = method(o)
assert result == 1               // Java would answer 2

assert method(42) == 2           // an Integer: only the Object overload applies

The Java version of the same program, run with JDK 26 (java Overloads.java), prints 2, because o is declared Object:

public class Overloads {
    static int method(String arg) { return 1; }
    static int method(Object arg) { return 2; }
    public static void main(String[] a) {
        Object o = "Object";
        System.out.println(method(o));
    }
}
2

The diagram contrasts the two decisions.

flowchart TD entry["method(o) where o is declared Object and holds a String"] entry --> javaSide entry --> groovySide subgraph javaSide ["Java: compile time"] j1["javac sees the declared type: Object"] --> j2["only method(Object) applies"] --> j3["result is 2"] end subgraph groovySide ["Groovy: run time"] g1["the call site looks at the actual argument types"] --> g2["String is an exact match for method(String)"] --> g3["result is 1"] end

Multi-methods is what makes Groovy’s duck typing work with overloads, but it has a cost to remember: adding an overload, or changing the runtime type of an argument, can silently change which method runs. With @CompileStatic (see Static Type Checking and @CompileStatic) the choice is made at compile time again and follows Java’s rule. The Java side is covered under Methods and Parameters.

Array initializers

In Java an array initializer is {1, 2, 3}. In Groovy a pair of braces is reserved for closures, so the shorthand does not compile. Use a list literal, which Groovy coerces to the declared array type. Since Groovy 3.0 the Java long form new int[] {1, 2, 3} also works.

int[] array = [1, 2, 3]                 // list literal coerced to int[]
def array2 = new int[] {4, 5, 6}        // Java long form, Groovy 3.0+
assert array.class == int[]
assert array2.class == int[]
assert array2.sum() == 15

def closure = { 1 + 1 }                 // braces mean a closure
assert closure() == 2

String[] names = ['a', 'b']
assert names.length == 2 && names[1] == 'b'
int[][] grid = [[1, 2], [3, 4]]
assert grid[1][0] == 3

The Java shorthand fails at compile time, because the parser reads a closure and trips on the comma:

arrays2.groovy: 1: Unexpected ',' @ line 1, column 17.
   int[] array = {1, 2, 3}
                   ^

Package scope visibility

In Java, a field without a modifier is package-private. In Groovy, omitting the modifier declares a property: a private field plus a public getter and setter. To get a real package-private field, annotate it with @PackageScope.

import groovy.transform.PackageScope
import java.lang.reflect.Modifier

class Person {
    String name                     // property: private field + getName() + setName()
    @PackageScope String code       // package-private field, no accessors
}

def p = new Person(name: 'Ada', code: 'x1')
assert p.name == 'Ada'
assert Person.getDeclaredField('name').modifiers == Modifier.PRIVATE
assert Person.declaredMethods.name.containsAll(['getName', 'setName'])
assert !Person.declaredMethods.name.contains('getCode')
assert Person.getDeclaredField('code').modifiers == 0   // no modifier: package-private

The reflection asserts all pass. Properties, final properties and direct field access are covered on Classes, Fields and Properties; the Java side is Classes and Objects.

ARM blocks (try-with-resources)

Java 7 introduced try-with-resources, which the Groovy documentation still calls ARM (Automatic Resource Management) blocks. Groovy supports the same syntax since 3.0. It also offers closure-based GDK methods that open, use and close the resource, which is usually shorter and is the idiomatic form.

import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.Path

Path file = Files.createTempFile('greenhouse', '.log')
Files.writeString(file, "sensor-1 21.5\nsensor-2 19.0\n")

// 1. Java-style try-with-resources (Groovy 3+)
try (BufferedReader reader = Files.newBufferedReader(file, Charset.forName('UTF-8'))) {
    String line
    while ((line = reader.readLine()) != null) {
        println "java style: $line"
    }
} catch (IOException e) {
    e.printStackTrace()
}

// 2. Idiomatic: the GDK closes the reader for you
file.toFile().eachLine('UTF-8') { println "eachLine: $it" }

// 3. Closer to Java
file.toFile().withReader('UTF-8') { reader ->
    reader.eachLine { println "withReader: $it" }
}
Files.delete(file)
java style: sensor-1 21.5
java style: sensor-2 19.0
eachLine: sensor-1 21.5
eachLine: sensor-2 19.0
withReader: sensor-1 21.5
withReader: sensor-2 19.0

Inner classes

Anonymous and nested classes follow Java closely, with one difference that comes from the implementation: Groovy generates the inner-class bytecode with machinery shared with groovy.lang.Closure, so local variables used from inside an inner class do not have to be final or effectively final.

Static inner classes

Static nested classes are the best-supported form. If you really need an inner class in Groovy code, make it static.

class A {
    static class B {}
}
assert new A.B() instanceof A.B

Anonymous inner classes

import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

CountDownLatch called = new CountDownLatch(1)
Timer timer = new Timer()
timer.schedule(new TimerTask() {
    void run() {
        called.countDown()
    }
}, 0)
assert called.await(10, TimeUnit.SECONDS)
timer.cancel()

The non-final capture is the visible difference. Java refuses to compile the next snippet ("local variables referenced from an inner class must be final or effectively final"); Groovy shares the variable with the inner class:

int count = 0
Runnable r = new Runnable() {
    void run() { count++ }       // legal in Groovy, rejected by javac
}
r.run(); r.run()
assert count == 2
println "count = $count"
count = 2

Creating instances of non-static inner classes

Java lets you write y.new X(). Before Groovy 3.0 that syntax was not available and you wrote new X(y), passing the enclosing instance as a hidden first argument. Groovy 3.0 and later accept the Java form, and the old form still works.

class Y {
    class X {}
    X foo() { new X() }
    static X createX(Y y) { y.new X() }      // Java syntax, Groovy 3.0+
    static X createXOld(Y y) { new X(y) }    // pre-3.0 style, still works
}
assert Y.createX(new Y()) instanceof Y.X
assert Y.createXOld(new Y()) instanceof Y.X

One trap remains from the old form. Groovy lets you call a one-parameter method or constructor without an argument (the parameter is then null), so writing new X() instead of new X(this) compiles and gives an inner instance with no enclosing object. The documentation says there is no good way to prevent this.

Lambda expressions and the method reference operator

Java 8’s lambdas and :: method references work in Groovy 3.0 and later (with the Parrot parser, which is the only parser since Groovy 4). Before 3.0 you had to use closures. Closures remain idiomatic Groovy: they are coerced to any single-abstract-method interface, and .& creates a method pointer.

Runnable run = () -> println 'Run'                    // lambda
run.run()
List<String> sensors = ['s1', 's2']
sensors.forEach(System.out::println)                  // method reference
def lengths = sensors.stream().map(String::length).toList()
assert lengths == [2, 2]
Runnable asClosure = { println 'run' }                // closure coerced to SAM
asClosure.run()
sensors.each { println it }
sensors.each(this.&println)                           // method pointer
Run
s1
s2
run
s1
s2
s1
s2

GStrings

A double-quoted literal is a String when it has no interpolation and a GString (groovy.lang.GString) when it does. Two consequences matter when mixing Java and Groovy:

  • A Java class that contains a double-quoted literal with a $ is compiled differently by the two compilers: with groovyc the $ starts an interpolation (or a compile error), with javac it is just a dollar sign. Use single quotes, or escape the dollar, in code that both compilers see.

  • Groovy converts a GString to String automatically when a method declares a String parameter, but not when the parameter is Object. A Java API that accepts Object and then checks the real type receives a GString.

A GString is also not a String as a map key: a map built with a GString key keeps it as a GString, and the lookup with a plain string does not find it.

def price = 12
def s = "cost: $price"
assert s instanceof GString
assert "plain" instanceof String

// an Object parameter that checks the actual type
Object describe(Object o) { o instanceof String ? 'String' : o.class.simpleName }
println describe("cost: $price")
println describe("cost: $price".toString())

// map keys
def m = ["k$price": 1]
println m.containsKey("k12")
println m.keySet()*.class
String dollar = 'costs $5'
println dollar
GStringImpl
String
false
[class org.codehaus.groovy.runtime.GStringImpl]
costs $5

Call toString() (or declare the variable String) before passing a GString to code that compares types or uses it as a key. Interpolation, the six string forms and lazy evaluation are on Strings and GStrings; the Java side is Strings and Text.

String and character literals

In Groovy, single quotes make a String, not a char. Double quotes make a String or a GString. There is no character literal; a one-character string is converted to char automatically only when assigned to a variable of type char. A method with a char parameter needs an explicit cast.

import org.codehaus.groovy.runtime.typehandling.GroovyCastException

assert 'c'.class == String
assert "c".class == String
assert "c${1}".class in GString

char a = 'a'
assert Character.digit(a, 16) == 10: 'But Groovy does boxing'
assert Character.digit((char) 'a', 16) == 10
try {
    assert Character.digit('a', 16) == 10
    assert false: 'Need explicit cast'
} catch (MissingMethodException e) {
    println 'MissingMethodException'
}

assert ((char) 'c').class == Character
assert ('c' as char).class == Character
try {
    ((char) 'cx') == 'c'
    assert false: 'will fail - not castable'
} catch (GroovyCastException e) {
    println 'GroovyCastException'
}
assert ('cx' as char) == 'c'
assert 'cx'.asType(char) == 'c'
println 'chars ok'
MissingMethodException
GroovyCastException
chars ok

Groovy has two cast styles. The C-style cast (char) 'cx' rejects a multi-character string with a GroovyCastException, while as char is lenient and takes the first character.

The == operator and equality

In Java, == compares primitive values and compares object references. In Groovy, == means equality everywhere:

  • for Comparable objects it evaluates a.compareTo(b) == 0;

  • for everything else it calls a.equals(b).

To test identity, use a.is(b), or the === and !== operators added in Groovy 3.0.

def a = new BigDecimal('1.0')
def b = new BigDecimal('1.00')
assert !a.equals(b)               // Java equals: scale matters
assert a == b                     // Groovy ==: compareTo(b) == 0
assert a.compareTo(b) == 0

def s1 = new String('abc')
def s2 = new String('abc')
assert s1 == s2                   // equals
assert !s1.is(s2)                 // identity
assert !(s1 === s2)               // identity operator, Groovy 3+
assert s1 !== s2
assert s1 === s1

def l1 = [1, 2]
def l2 = [1, 2]
assert l1 == l2
assert l1 !== l2

class Plain { }
assert new Plain() != new Plain()     // no equals override: identity
println 'eq ok'

A second consequence: Java’s "x" == new String("x") is false, while the same text in Groovy is true. This difference is easy to trip over in code ported from Java in either direction. Groovy’s + also behaves like Java’s on strings ("a" + 1 + 2 is a12 in both languages), so string concatenation is not a difference.

Source: Groovy documentation: Differences with Java. All operators are on Arithmetic, Relational and Logical Operators, the identity and other object operators on Object Operators, and compareTo-based ordering on Operator Overloading.

Primitives and wrappers

Java treats primitives (int, boolean, double) as special and auto-boxes them when an object is needed. Groovy goes further and tries to treat everything as an object: a primitive variable or field can be used like an object and is boxed as needed. The first example is valid in both languages, since boxing and unboxing work the same way when the parameters are passed:

class Main {
    float f1 = 1.0f
    Float f2 = 2.0f
    float add(Float a1, float a2) { a1 + a2 }
    Float calc() { add(f1, f2) }
}
assert new Main().calc() == 3.0

The second one differs. Dereferencing a primitive does not compile in Java ("float cannot be dereferenced"), but works in Groovy:

class Holder { float z1 = 0.0f }
assert !(new Holder().z1.equals(1.0f))

Boxing beats widening

Java prefers widening over boxing when choosing an overload. Because Groovy uses the wrapper classes in more places, it does not follow that rule. With a primitive int argument and the overloads m(long) and m(Integer), Java calls the widening one and Groovy calls the wrapper one:

int i = 0
println m(i)
String m(long l) { 'm(long)' }
String m(Integer i) { 'm(Integer)' }
m(Integer)

The same program written in Java (with static methods) prints m(long) on JDK 26.

Numeric primitive optimisation with @CompileStatic

Boxing in more places could suggest slower arithmetic. Groovy has a highly optimised set of classes for dynamic arithmetic, and with @CompileStatic an expression that involves only primitives compiles to the same bytecode javac would emit. Disassembling two identical methods, one annotated and one not, shows it:

import groovy.transform.CompileStatic

class Sums {
    @CompileStatic
    static long fast(int n) {
        long total = 0
        for (int i = 1; i <= n; i++) { total += (long) i * i }
        total
    }
    static long dynamic(int n) {
        long total = 0
        for (int i = 1; i <= n; i++) { total += (long) i * i }
        total
    }
}

javap -c -p on the class compiled with groovyc shows, for fast, plain primitive instructions, and for dynamic, boxing and dynamic call sites (invokedynamic, Long.valueOf, longUnbox). The excerpt below is abridged from that output:

fast:     ... lmul ... ladd ...
dynamic:  Long.valueOf ... invokedynamic ... compoundAssign ... DefaultTypeTransformation.longUnbox ...

Positive and negative zero

IEEE 754 distinguishes 0.0 from -0.0. For primitives, Java compares with a bytecode instruction for which the two zeros are equal; for the wrapper classes, Float.equals returns false for this pair. Java run on JDK 26:

float f1 = 0.0f, f2 = -0.0f     ->  f1 == f2           true
Float g1 = 0.0f, g2 = -0.0f     ->  g1.equals(g2)      false

Groovy switches between primitives and wrappers in more places, so the safest rule is to be explicit:

  • to distinguish the zeros, use equals or cast primitives to the wrapper type before ==;

  • to ignore the sign, use the GDK method equalsIgnoreZeroSign or cast wrappers to the primitive type before ==.

float f1 = 0.0f
float f2 = -0.0f
Float f3 = 0.0f
Float f4 = -0.0f

assert f1 == f2
assert (Float) f1 != (Float) f2
assert f3 != f4              // for non-primitives == maps to equals()
assert (float) f3 == (float) f4
assert !f1.equals(f2)
assert !f3.equals(f4)
assert f1.equalsIgnoreZeroSign(f2)
assert f3.equalsIgnoreZeroSign(f4)
println 'zero ok'
zero ok

Conversions

Java converts automatically only when widening, needs a cast to narrow and silently truncates when it does. Groovy accepts many more conversions. The official page has two matrices (primitives and wrappers, plus BigInteger and BigDecimal); they are summarised here by what they mean, then the notable ones are demonstrated.

Code in the official matrix Meaning

Y

A conversion Groovy can make (in Java’s matrix: one Java can make).

C (Java only)

Needs an explicit cast.

D (Groovy only)

Allowed when the code is compiled dynamically, or with an explicit cast (so @CompileStatic rejects it without a cast).

T

Allowed, but data is truncated.

B

A boxing or unboxing operation.

N

Not possible.

The rules the page gives in prose:

  • conversion to boolean or Boolean truncates to the Groovy Truth;

  • converting a number to a character casts Number.intValue() to char;

  • BigInteger and BigDecimal are built from Number.doubleValue() when the source is a Float or Double, otherwise from toString();

  • other conversions follow the behaviour defined by java.lang.Number.

// Java's table: narrowing needs a cast and truncates silently
long big = 3_000_000_000L
int narrowed = (int) big
println "long -> int: $narrowed"                       // T: data truncated

// Groovy: a number to boolean uses the Groovy Truth
boolean fromInt = (boolean) 5
boolean fromZero = (boolean) 0
println "5 -> $fromInt, 0 -> $fromZero"

// a number to char uses Number.intValue()
char c = (char) 66
char d = 67 as char
println "$c $d"

println((Double) 2.9d as int)                          // truncates, does not round

// D: dynamic or explicit cast
def x = 200
byte b = x as byte            // narrowing int -> byte, wraps
println "200 as byte = $b"
Integer boxed = 5
long widened = boxed          // Y: Integer -> long
println widened.class.simpleName
// N: boolean to a number never converts
try { int bad = (int) true } catch (e) { println e.class.simpleName }
long -> int: -1294967296
5 -> true, 0 -> false
B C
2
200 as byte = -56
Long
GroovyCastException

The D conversions are where dynamic and static compilation differ. This code is accepted by a plain script, and with @CompileStatic only after the explicit cast:

import groovy.transform.CompileStatic

@CompileStatic
void tryIt() {
    Long l = 5L
    int i = l                 // implicit narrowing: the checker rejects it
}
conv3.groovy: 5: [Static type checking] - Possible loss of precision from long to int
 @ line 5, column 13.
       int i = l                 // implicit narrowing: the checker rejects it

Writing int i = (int) l compiles and runs. Promotion rules for arithmetic and how as and asType coerce are on Type Coercion and Promotion; Java’s conversions are on Primitive Types and Variables.

Extra keywords

Groovy has most of Java’s keywords, and since Groovy 3 it also has Java’s reserved type name var. On top of Java’s, the following words have special meaning:

  • as

  • def

  • in

  • trait

  • it (inside closures, as the implicit parameter)

Groovy is more lenient than Java about where some of these may appear, so this is legal:

var var = [def: 1, as: 2, in: 3, trait: 4]
assert var.def == 1 && var.as == 2 && var.in == 3 && var.trait == 4
println var

[1, 2, 3].each { println it * 2 }              // implicit `it` inside a closure
def it = 'declared'                            // legal, but confusing
println it

assert 2 in [1, 2, 3]
def n = 5 as String
assert n == '5'
trait Named { String name() { 'x' } }
class Rose implements Named {}
assert new Rose().name() == 'x'
[def:1, as:2, in:3, trait:4]
2
4
6
declared

Being legal does not make it wise: avoid these words as variable, method and class names even when the compiler accepts them.

Groovy 6.0 adds one more contextual keyword, val, which declares a final variable or field, a companion to var (GEP-16). It is contextual, so existing programs that call a variable val still compile, but an attempt to declare val val collides. Reassigning a val is a compile error:

val x = 1
println x
x = 2         // The variable [x] is declared final but is reassigned

The full list of reserved and contextual keywords is on Lexical Structure, Comments, Keywords and Identifiers, and def, var and val on Variables, Typing, def, var and val.

Summary table

Topic Java Groovy

Default imports

java.lang only

java.io, java.lang, java.math (two classes), java.net, java.time (5.0), java.util, groovy.lang, groovy.util

Overload choice

Compile time, declared types

Run time, actual types (compile time under @CompileStatic)

Array literal

{1, 2, 3}

[1, 2, 3]; new int[] {...} since 3.0

Field without modifier

Package-private

Property (private field, getter and setter); use @PackageScope

try-with-resources

Since Java 7

Since Groovy 3.0; also withReader, eachLine

Inner class captures

Effectively final locals only

Any local

y.new X()

Yes

Since 3.0 (before: new X(y))

Lambdas and ::

Since Java 8

Since 3.0 (before: closures)

"...$x..."

A plain string

A GString

'c'

A char

A String

==

Primitive value or reference

compareTo or equals; === for identity (3.0)

Primitive overloads

Widening before boxing

Boxing; m(Integer) before m(long)

0.0f == -0.0f

true for primitives, false for equals

Same rules, use equalsIgnoreZeroSign to ignore the sign

Narrowing conversions

Cast required

Allowed dynamically, cast needed under @CompileStatic

Extra keywords

none

as, def, in, trait, it; val (6.0)

Where older books and tutorials are outdated

  • Books written for Groovy 1.x and 2.x predate most of the "since" entries above: no Java-style try-with-resources, no y.new X(), no lambdas or ::, no ===, and no new int[] {...}. Code that avoided them still runs.

  • Older material lists java.lang, java.util, java.io, java.net, groovy.lang and groovy.util plus the two java.math classes as the default imports. java.time.* is new in Groovy 5.0.

  • Old tutorials describe the pre-3.0 parser and tell you to avoid Java syntax such as the lambda arrow; the Parrot parser has been the only parser since Groovy 4.0.

  • The val keyword (6.0) is not in any printed book.

  • The first-edition book used for this section targets Groovy 1.0; see the bibliography for the title and how it was used.