Inheritance, Interfaces & Generics
|
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 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26 (checked on 2026-10-14). Groovy classes are ordinary JVM classes, so single class inheritance, interfaces and generics work as on Java Inheritance & Polymorphism, Java Interfaces and Java Generics. What differs is mostly what is checked and when: Groovy resolves calls on the runtime type, treats interfaces as optional for dynamic code, and does not enforce generics unless the code is statically compiled. Classes, fields and properties are on Classes, Fields & Properties; constructors and methods on Methods, Constructors & Parameters; reusable behaviour that is neither a class nor an interface on Traits.
Superclasses and abstract classes
A class extends at most one other class with extends. Every class without an explicit superclass extends
java.lang.Object. Overriding needs no keyword; @Override is optional but makes the compiler check that a superclass
method really exists. super calls the superclass version, and the first statement of a constructor can be
super(...) to choose the superclass constructor.
An abstract class may declare abstract methods without a body and may not be instantiated. Groovy rejects an instantiation
it can see at compile time.
abstract class Sensor {
String id
Sensor(String id) { this.id = id }
abstract double read()
String describe() { "Sensor ${id}" }
}
class TemperatureSensor extends Sensor {
TemperatureSensor(String id) { super(id) }
double read() { 21.5 }
@Override String describe() { super.describe() + " (temperature)" }
}
def s = new TemperatureSensor('t-1')
println s.describe()
println s.read()
println s instanceof Sensor
println TemperatureSensor.superclass.simpleName
println 'abstract' + ' ' + java.lang.reflect.Modifier.isAbstract(Sensor.modifiers)
Sensor t-1 (temperature)
21.5
true
Sensor
abstract true
Adding new Sensor() to the script stops compilation:
e9.groovy: 2: You cannot create an instance from the abstract class 'Sensor'.
@ line 2, column 1.
new Sensor()
^
Method calls are always dispatched on the runtime type of the receiver, and the same holds for method arguments: Groovy
selects among overloads using the runtime types of the arguments (multi-methods), which Java does not. The call below passes
a variable declared Object, yet the String overload runs.
class Base { String who() { 'base' }; String call() { who() } }
class Derived extends Base { String who() { 'derived' } }
println new Derived().call()
class Printer {
String p(Object o) { 'object' }
String p(String s) { 'string' }
}
Object o = 'text'
println new Printer().p(o)
derived
string
Under @CompileStatic overloads are chosen from the declared types as in Java. Wrapping the call in methods whose parameter
is declared Object shows the difference (run with Groovy 6.0.0):
import groovy.transform.CompileStatic
@CompileStatic
String viaStatic(Object o) { new Printer().p(o) }
String viaDynamic(Object o) { new Printer().p(o) } // Printer as above
println viaDynamic('text')
println viaStatic('text')
string
object
Interfaces
An interface declares the methods a class must provide. Groovy interfaces can hold methods and constants, but not
properties or mutable state. Since Groovy 5.0 an interface can also contain default, private and static methods with a
body, as in Java 8 and 9.
The default method below calls a private helper. In Groovy 6.0.0 this works when the interface is compiled statically
(@CompileStatic on the interface, as in the example); in plain dynamic code the same call from a default method to a private
interface method failed at runtime with MissingMethodException when tried here, so do not rely on it without
@CompileStatic.
import groovy.transform.CompileStatic
@CompileStatic
interface Actuator {
String name()
default String status() { "${name()} is ${state()}" }
private String state() { 'idle' }
static Actuator of(String n) { { -> n } as Actuator }
}
class Fan implements Actuator {
String name() { 'fan' }
}
println new Fan().status()
println Actuator.of('valve').name()
fan is idle
valve
Without the private helper, default and static interface methods work in dynamic Groovy too:
interface Actuator {
String name()
default String status() { "${name()} is on" }
static Actuator create(String n) { new Named(n) }
}
class Named implements Actuator {
String n; Named(String n) { this.n = n }
String name() { n }
}
println Actuator.create('valve').status()
valve is on
A class implements any number of interfaces with implements, after the optional extends clause. A class that does not
supply every abstract method must be declared abstract.
interface Waterer { String water() }
interface Lighter { String light() }
class Plant { String grow() { 'growing' } }
class SmartPlant extends Plant implements Waterer, Lighter {
String water() { 'watered' }
String light() { 'lit' }
}
def p = new SmartPlant()
println p.grow() + ' ' + p.water() + ' ' + p.light()
println SmartPlant.interfaces*.simpleName
println Waterer.isAssignableFrom(SmartPlant)
growing watered lit
[Waterer, Lighter]
true
Interfaces cannot carry state or per-class behaviour that is not a method signature; when shared behaviour and fields are
needed, use a trait (Traits). Groovy also supports sealed hierarchies, which
restrict who may implement an interface; see Records & Sealed Types.
Coercing closures and maps to interfaces
An interface with one abstract method (a single abstract method, SAM) can be implemented by a closure, and a map of closures
can implement an interface with several methods. In both cases the object produced by as is a runtime proxy, so it works
without declaring a class.
interface Speaker { String speak() }
Speaker s1 = { 'hello' } as Speaker
Speaker s2 = [speak: { 'from map' }] as Speaker
println s1.speak()
println s2.speak()
hello
from map
Assigning a closure directly to a SAM-typed variable (Speaker s = { 'hello' }) is also accepted. See
Closures and Type Coercion & Promotion.
Duck typing versus interfaces
In dynamic Groovy a call needs only that the receiver has the method at runtime; no common supertype is required. Interfaces are therefore optional for dynamic code: they document a contract and make type-checked code possible, but unrelated classes with the same method can be used interchangeably.
class Duck { String speak() { 'quack' } }
class Robot { String speak() { 'beep' } }
String talk(thing) { thing.speak() } // no declared type: duck typing
println talk(new Duck())
println talk(new Robot())
try { talk('text') } catch (e) { println e.class.simpleName }
quack
beep
MissingMethodException
The price is that a missing method is discovered only when the call runs. Use a declared interface type (and
@CompileStatic or @TypeChecked) wherever that early failure matters; see
Static Type Checking & @CompileStatic. Duck typing
also does not give instanceof: an object that merely has a speak() method is not an instance of Speaker.
Generics
Groovy supports the same generics syntax as Java: generic classes and methods, bounded type parameters (T extends
Comparable<T>), and wildcards (?, ? extends, ? super). Generics are erased at runtime, as in Java, and Groovy’s
dynamic mode does not enforce them: the compiler treats the type arguments as documentation and for tooling.
class Box<T extends Comparable<T>> {
private final List<T> items = []
void add(T item) { items << item }
T max() { items.max() }
}
def b = new Box<Integer>()
b.add(3); b.add(9); b.add(4)
println b.max()
// generics are erased: no check in dynamic Groovy
List<Integer> ints = [1, 2]
ints << 'oops'
println ints
println ints*.class*.simpleName
static <T> List<T> twice(T x) { [x, x] }
println twice('a')
println Box.typeParameters[0].bounds*.typeName
def sum = { List<? extends Number> l -> l.sum() }
println sum([1, 2.5])
9
[1, 2, oops]
[Integer, Integer, String]
[a, a]
[java.lang.Comparable<T>]
3.5
The list declared List<Integer> accepts a String and nobody complains until a consumer expects an Integer. The bound on
the type parameter is real class-file metadata (typeParameters), which is what tools and Java callers see.
Compile the same statement with @CompileStatic (or @TypeChecked) and the type argument is enforced:
import groovy.transform.CompileStatic
@CompileStatic
void test() {
List<Integer> ints = [1, 2]
ints << 'oops'
}
test()
Cannot call <T> org.codehaus.groovy.runtime.DefaultGroovyMethods#leftShift(java.util.List<T>, T)
with arguments [java.util.ArrayList<java.lang.Integer>, java.lang.String]
(The message is a single line in the real output; it is wrapped here.) That is the practical rule for generics in Groovy:
declare them for documentation and Java interop in dynamic code, and add @CompileStatic or @TypeChecked where the compiler
should enforce them.
JLS-compliant generic syntax in Groovy 6
Groovy 6.0 aligned its handling of generic types with the Java Language Specification: rare types, nested parameterizations,
instanceof against reifiable types and type-variable scoping now behave as javac does, with matching diagnostics (see the
Groovy 6.0 release notes). In practice, code that is valid Java generics is accepted as such, and invalid generics are
reported with Java-style messages.
Map<String, List<? extends Number>> readings = [temp: [21, 22.5], humidity: [60]]
readings.each { k, v -> println "$k -> ${v.sum()}" }
Object o = [1, 2]
println(o instanceof List<?>)
temp -> 43.5
humidity -> 60
true
Source: Groovy 6.0 release notes
Where the book is outdated
Older books treat interfaces as method signatures only, because they predate Groovy’s native support for default, private and
static interface methods (available since Groovy 5.0). They also describe generics as always
unchecked, which is still true for dynamic code but not for code under @CompileStatic/@TypeChecked, and predate the
Groovy 6.0 alignment with the JLS. Traits were introduced in Groovy 2.3 and are the Groovy answer to sharing implementation across classes.