Groovy & Other Languages

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), run on JDK 26 and checked on 2026-10-14. It is a translation table for readers who already know another language: for each everyday idiom it shows the Groovy form next to the form in Java, Kotlin, Scala, Python and Ruby, runs the Groovy side, and then discusses what dynamic and static typing cost and buy you. It deliberately stays at the level of idioms. The exact differences from Java, row by row, are on Differences with Java.

The other languages are shown only as far as needed to make the comparison readable; their own sections go further. The Kotlin and Python references on this site are Kotlin and Python. The Scala (#299) and Ruby (#276) references are tracked as separate issues and are mentioned here by number only, because their pages do not exist yet.

Where Groovy sits

Groovy is an optionally typed language for the JVM. Its source compiles to ordinary JVM classes, so it calls Java libraries and is called from Java with no wrapper layer. It keeps Java’s braces, classes and packages, and adds closures, literals for lists, maps and ranges, operators such as ?. and ?:, string interpolation, properties and a large library of methods added to existing JDK types (the GDK). Whether a given line is checked at compile time or at run time is your choice, line by line or class by class (see Dynamic versus static typing).

Compared with Short answer

Java

Almost every Java source file is already valid Groovy; Groovy removes ceremony and adds dynamic features. See the row-by-row list of differences.

Kotlin

Both are concise JVM languages with Java-like syntax and lambdas. Kotlin is statically typed with null-safety in the type system; Groovy is dynamic by default with run-time null-safe operators, and has stronger metaprogramming.

Scala

Scala is statically typed with a far richer type system and functional emphasis. Groovy trades type-level guarantees for less ceremony and for scripting.

Python

Groovy is the closest JVM equivalent of Python’s scripting style: dynamic, concise, with literals and a batteries-included library, but with braces and the JVM’s performance and ecosystem.

Ruby

Groovy’s closures, open classes, builders and optional parentheses are close to Ruby’s blocks, open classes and DSL style. Ruby is dynamic only; Groovy can also be compiled statically.

Idiom mapping

The tables list the idiom as it is written in each language. Code in the Groovy column is real Groovy 6.0; the other columns are the usual spelling in a current release of that language and are given for orientation only.

Collection literals

Language List and map literals

Java

List.of(1, 2) and Map.of("a", 1) create immutable collections; new ArrayList<>() plus add for mutable ones

Kotlin

listOf(1, 2), mutableListOf(1, 2), mapOf("a" to 1)

Scala

List(1, 2), Map("a" -> 1)

Python

[1, 2], {"a": 1}

Ruby

[1, 2], { a: 1 }

Groovy

[1, 2] is a mutable java.util.ArrayList; [a: 1] is a mutable java.util.LinkedHashMap whose unquoted key is the string 'a'; [1, 2] as Set, 1..5 and [] as int[] give a set, a range and an array

// Collection literals, closures, spread and the GDK
def temperatures = [18.4, 18.9, 19.6]
def zones = [tomatoes: 'bench-1', basil: 'basket-1']
def hot = temperatures.findAll { it > 18.5 }
def names = zones.collect { plant, zone -> "$plant@$zone" }

assert temperatures.class == ArrayList
assert zones.getClass() == LinkedHashMap
assert hot == [18.9, 19.6]
assert names == ['tomatoes@bench-1', 'basil@basket-1']
assert zones*.key == ['tomatoes', 'basil']
assert temperatures.sum() / temperatures.size() == 18.9666666667
println temperatures.sum() / temperatures.size()
println temperatures.sum().getClass().name

Output of the two println calls, produced by running the script:

18.9666666667
java.math.BigDecimal

Two details are worth noting. Decimal literals such as 18.4 are BigDecimal, not double, so the average is exact to ten decimals and printed with a limited scale; this differs from Java, Kotlin, Python and Ruby, where 18.4 is a binary floating-point value. The spread operator *. calls the property or method on every element and collects the results, which is what a Python list comprehension or a Ruby map(&:key) does.

Closures and lambdas

Language Anonymous function that doubles its argument

Java

x -> x * 2 (a lambda, bound to a functional interface)

Kotlin

{ x -> x * 2 }, or { it * 2 } for one parameter

Scala

x => x * 2 or _ * 2

Python

lambda x: x * 2 (expression only; longer bodies need def)

Ruby

{ |x| x * 2 } (a block), ->(x) { x * 2 } (a lambda)

Groovy

{ x -> x * 2 }, or { it * 2 } for one parameter; Java-style x -> x * 2 lambdas also compile since Groovy 3.0

A Groovy closure is an object of class groovy.lang.Closure. Unlike a Java lambda it carries an owner and a delegate that decide where unqualified names are resolved, and that is what makes builders and DSLs possible (see Closure Delegation: Owner, Delegate & This). A closure is converted automatically to a functional interface when passed to a Java API that expects one. For Java lambdas see Lambdas & Method References; the Groovy closures are covered on Closures.

Null safety and defaults

Language Reading a possibly-null chain, with a default

Java

Optional.ofNullable(a).map(A::b).orElse("none"), or an explicit a != null test

Kotlin

a?.b ?: "none"; the type system distinguishes String from String?

Scala

Option(a).map(_.b).getOrElse("none")

Python

a.b if a is not None else "none"; or also replaces other falsy values

Ruby

a&.b || "none"

Groovy

a?.b ?: 'none'; the safe-navigation operator ?. short-circuits on null and the Elvis operator ?: supplies a default for any value that is false under the Groovy truth

class Zone { String name; Zone parent }

def bench = new Zone(name: 'bench-1')
assert bench.parent?.name == null            // safe navigation
assert (bench.parent?.name ?: 'none') == 'none' // Elvis
assert bench.parent?.name?.size() == null
String maybe = null
assert (maybe ?: 'default') == 'default'
// Groovy truth: 0, '' and empty collections are false
assert !0 && !'' && ![] && ![:] && !null
assert 'x' && [1] && 1

The script ran without output and with every assertion true.

The caveat is the Groovy truth: ?: replaces every value that is false, not only null. 0, the empty string, an empty collection and an empty map are all false, exactly as in Python and unlike Kotlin or Ruby, where only null and false are. Use ?. plus an explicit == null test when 0 or '' is a legitimate value. Nothing in Groovy’s types stops a null from reaching a method call; the failure shows up as a NullPointerException or a MissingMethodException at run time, unless a static checker is active (see Dynamic versus static typing).

String interpolation

Language Embedding values in text

Java

"Water %s every %d hours".formatted(name, hours); text blocks for multiline strings

Kotlin

"Water $name every ${hours * 2} hours"

Scala

s"Water $name every ${hours * 2} hours"

Python

f"Water {name} every {hours * 2} hours"

Ruby

"Water #{name} every #{hours * 2} hours"

Groovy

"Water $name every ${hours * 2} hours" in a double-quoted or triple-double-quoted string; single-quoted strings never interpolate

import groovy.transform.ToString
import groovy.transform.EqualsAndHashCode

@ToString @EqualsAndHashCode
class Plant {
    String name
    int everyHours = 12
}

def tomatoes = new Plant(name: 'tomatoes', everyHours: 2)
tomatoes.everyHours += 1          // calls the generated getter and setter
println tomatoes
assert tomatoes == new Plant(name: 'tomatoes', everyHours: 3)
def name = 'basil'
def hours = 12
println "Water $name every ${hours} hours; total ${hours * 2}"
println """\
  zone:  bench-1
  plant: $name"""
Plant(tomatoes, 3)
Water basil every 12 hours; total 24
  zone:  bench-1
  plant: basil

A double-quoted string containing $ is a GString, a lazily evaluated object that is not a java.lang.String. It converts to String when you assign it to a String variable or pass it to a method that takes one, but it has a different hashCode, so a GString used as a Map key or compared with equals to a String surprises newcomers (covered on Strings & GStrings).

Properties and constructors

Language A mutable property with a default value

Java

a private field with getX/setX (or a record, which is immutable); constructors or a builder for named values

Kotlin

class Plant(var name: String, var everyHours: Int = 12), constructed with named arguments

Scala

case class Plant(name: String, everyHours: Int = 12), with named arguments

Python

@dataclass class Plant: name: str; every_hours: int = 12, or @property for computed values

Ruby

attr_accessor :name, with initialisation in initialize

Groovy

String name with no access modifier in a class declares a private field plus public getter and setter; new Plant(name: 'x') is the named-argument constructor; @ToString, @EqualsAndHashCode and similar annotations generate the boilerplate

The example above shows all of it: tomatoes.everyHours += 1 reads and writes the property through its generated accessors, and @ToString/@EqualsAndHashCode supply toString() and value equality. Declaring a property with a modifier or a field with an explicit visibility changes what is generated. Explicit getter and setter methods you write yourself win over the generated ones. The full rules are on Classes, Fields & Properties.

Extension methods and open classes

Language Adding behaviour to a type you do not own

Java

not possible; use a static utility method (Objects.requireNonNull, Collections.sort)

Kotlin

extension functions, fun String.shout() = uppercase() + "!", resolved statically

Scala

implicit classes (Scala 2) or extension methods (Scala 3)

Python

plain functions or subclassing; assigning to a built-in type is not allowed, and monkey-patching your own classes is discouraged

Ruby

open classes (class String; def shout; ...; end; end) and refinements

Groovy

categories with use, ExpandoMetaClass (String.metaClass.shout = { -> ... }) and extension modules; the GDK itself is built this way

class PlantCategory {
    static String label(String self) { self.capitalize() + ' (plant)' }
}

use(PlantCategory) {
    assert 'basil'.label() == 'Basil (plant)'
}
// outside the use block the method does not exist
try {
    'basil'.label()
} catch (MissingMethodException e) {
    println e.message.readLines().first()
}
// ExpandoMetaClass: Ruby-style open classes
String.metaClass.shout = { -> delegate.toUpperCase() + '!' }
assert 'water'.shout() == 'WATER!'
No signature of method: label for class: java.lang.String is applicable for argument types: () values: []

A category is scoped to the code block and the current thread; an ExpandoMetaClass change is global for the JVM until it is removed; an extension module is global and also visible to the static compiler. See Metaclasses, ExpandoMetaClass & Categories and Extension Modules. Kotlin’s extension functions are compile-time sugar for static functions, so unlike Groovy’s they cannot be added at run time or override a member.

Domain-specific languages

Language Mechanism

Java

fluent builders and method chaining; no operator or syntax customisation

Kotlin

lambdas with receivers, infix functions, operator overloading (see Type-safe Builders & DSLs)

Scala

infix calls, implicit conversions, by-name parameters, macros

Python

operator overloading, decorators, context managers

Ruby

blocks run with instance_eval, optional parentheses and method_missing

Groovy

optional parentheses and dots (command chains), closures with a delegate, operator overloading and AST transformations

// Parentheses and dots are optional in a chain of commands:
//   water 'tomatoes' every 2 hours   ==   water('tomatoes').every(2).hours
def water(String plant) {
    [every: { int n -> [hours: "$plant every $n hours"] }]
}

def line = water 'tomatoes' every 2 hours
assert line == 'tomatoes every 2 hours'
println line
tomatoes every 2 hours

This is the shape of the greenhouse watering DSL built on Domain Specific Languages. The same mechanism is why the Gradle Groovy DSL, Spock specifications and Jenkins pipeline scripts read like configuration. The Ruby analogue is covered by the Ruby reference tracked as #276.

Dynamic versus static typing

Groovy is dynamic by default: a method call is resolved when the line executes, by looking at the run-time types of the receiver and the arguments (multi-methods). That is the same model as Python and Ruby and it gives the same benefits and the same costs. Adding @CompileStatic or @TypeChecked to a class or a method switches that code to a Java-like model in which the compiler checks the call.

import groovy.transform.CompileStatic

def dynamic(x) { x.size() }       // resolved at run time
assert dynamic('basil') == 5
assert dynamic([1, 2]) == 2

@CompileStatic
int checked(String s) { s.size() }  // resolved at compile time

assert checked('basil') == 5

try {
    dynamic(42)
} catch (MissingMethodException e) {
    println e.message.readLines().first()
}
No signature of method: size for class: java.lang.Integer is applicable for argument types: () values: []

The same mistake in a @CompileStatic method is rejected before the script starts:

statictrap.groovy: 3: [Static type checking] - Cannot find matching method java.lang.String#sizee(). Please check if the declared type is correct and if the method exists.
 @ line 3, column 21.
Aspect Dynamic Groovy @TypeChecked / @CompileStatic

Errors such as a misspelt method

found when the line runs (MissingMethodException)

found at compile time

Duck typing and def parameters

natural

still allowed, but the call must type-check

Runtime metaprogramming (categories, ExpandoMetaClass, methodMissing)

works

@CompileStatic silently ignores it; @TypeChecked needs a type-checking extension

Performance

dispatch through the metaclass and invokedynamic

direct bytecode like Java, with @CompileStatic

Refactoring and IDE support

weaker

close to Java

The usual practice, reflected in the official style guide, is to leave scripts, tests and DSL code dynamic, and to put @CompileStatic on library and service code where a type error should stop the build. The mechanisms and their limits are on Static Type Checking & @CompileStatic and Type Checking Extensions.

Where Groovy fits on the JVM

Use Why Groovy

Scripting and automation

groovy runs a source file directly, @Grab downloads dependencies, and the GDK covers files, processes, JSON and XML (see Running Scripts & the groovy Command)

Build files

the Gradle Groovy DSL and Ant-style builders; Gradle also has a Kotlin DSL

Tests

Spock specifications and JUnit tests with power assertions read compactly

Embedded scripting

a Java application can evaluate Groovy at run time with GroovyShell (see Integrating Groovy into Java Applications)

Data wrangling

literals plus collect, groupBy, JsonSlurper and GINQ shorten transformations

Large statically typed code bases

possible with @CompileStatic, but Java or Kotlin are the more usual first choice

The scenario for the rest of the section, the greenhouse toolkit, plays to these strengths: reading a sensor log, building a schedule from a small DSL and scripting a report. The same code in Java would be several times longer; in Kotlin it would be about as long but would not run as a plain script file.

Where the book is outdated

The first edition of the reference book (Groovy 1.0, 2007) predates most features compared here. It has no lambdas or method references, no ?[] index-safe operator, no traits, no @CompileStatic, and its idiom for extending types is ExpandoMetaClass or a category. Groovy 1.0 also had no annotations, enums or generics in the examples, since it ran on JDK 1.4. Today the book’s comparisons to Java should be read against the Groovy 6.0 rows above. The one-line summary of each change is collected on What’s New & Migrating from Older Versions.