Inheritance and Interfaces
Prerequisites: Classes and objects
Inheritance, abstract classes, and interfaces work as in Java.
abstract class Shape {
String name
abstract BigDecimal area()
String describe() { "$name with area ${area().round(2)}" }
}
class Circle extends Shape {
BigDecimal radius
Circle(BigDecimal radius) { this.name = 'circle'; this.radius = radius }
BigDecimal area() { Math.PI * radius ** 2 }
}
class Rectangle extends Shape {
BigDecimal width, height
Rectangle(BigDecimal w, BigDecimal h) { name = 'rectangle'; width = w; height = h }
BigDecimal area() { width * height }
}
class Square extends Rectangle {
Square(BigDecimal side) { super(side, side); name = 'square' }
}
[new Circle(1), new Rectangle(2, 3.5), new Square(4)].each {
println it.describe()
}
circle with area 3.14
rectangle with area 7.00
square with area 16.00
extendsinherits from one class;super(...)calls the parent constructor.- An
abstractmethod must be implemented by subclasses. - Calling
describe()runs the parent's method, which calls each subclass's ownarea()— polymorphism.
Interfaces
interface Notifier {
void send(String message)
default void sendAll(List<String> messages) {
messages.each { send(it) }
}
}
class ConsoleNotifier implements Notifier {
void send(String message) { println "[console] $message" }
}
class PrefixNotifier implements Notifier {
String prefix
void send(String message) { println "$prefix $message" }
}
List<Notifier> notifiers = [new ConsoleNotifier(), new PrefixNotifier(prefix: '>>')]
notifiers.each { it.sendAll(['build ok', 'deployed']) }
[console] build ok
[console] deployed
>> build ok
>> deployed
Interfaces can have default methods, as in Java 8+.
Closures as interface implementations
interface Validator {
boolean validate(String input)
}
Validator notEmpty = { it?.trim() } as Validator
Validator digitsOnly = { String s -> s ==~ /\d+/ } // automatic coercion
println notEmpty.validate(' ')
println digitsOnly.validate('2026')
def comparator = { a, b -> a.size() <=> b.size() } as Comparator
def words = ['ccc', 'a', 'bb']
words.sort(comparator) // Java's List.sort: sorts in place, returns nothing
println words
println(['dddd', 'b', 'cc'].toSorted(comparator))
false
true
[a, bb, ccc]
[b, cc, dddd]
Careful: with a real Comparator argument, sort resolves to Java's
List.sort(Comparator), which returns void — so println list.sort(cmp)
would print null. toSorted always returns a new sorted list.
A closure can stand in for any interface with a single abstract method
(a "SAM" type) — either with as or automatically when assigned to a
typed variable or passed to a typed parameter. That's how Groovy
closures work with Java APIs that expect Runnable, Comparator, or
Function.
Checking types at runtime
def things = [42, 'text', 3.5, [1, 2], null]
things.each {
def kind = switch (it) {
case Number -> 'number'
case CharSequence -> 'text'
case Collection -> 'collection'
default -> 'other'
}
println "${it.inspect()} is $kind"
}
42 is number
'text' is text
3.5 is number
[1, 2] is collection
null is other