Skip to content
AgileLogics
6 min read

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
  • extends inherits from one class; super(...) calls the parent constructor.
  • An abstract method must be implemented by subclasses.
  • Calling describe() runs the parent's method, which calls each subclass's own area() — 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