Groovy Generics and Type Parameters – 13 Tested Examples

Groovy generics and type parameters with 13 examples. Covers generic classes, methods, bounded types, wildcards, @CompileStatic. Groovy 5.x.

“Generics are the seatbelts of type safety – you don’t always feel them, but they save you when things go wrong.”

Joshua Bloch, Effective Java

Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate to Advanced | Reading Time: 25 minutes

If you’ve ever worked with typed collections in Java, you already know what Groovy generics look like on the surface. But here’s the thing – Groovy treats generics very differently under the hood. In dynamic Groovy, generic type parameters are essentially optional hints. The compiler won’t stop you from putting a String into a List<Integer>. But when you flip on @CompileStatic, those same generics become enforced contracts, just like in Java.

This post covers how to write generic classes and methods in Groovy, use bounded type parameters with extends and super, work with wildcard types, understand type erasure on the JVM, and know exactly when Groovy enforces generics vs when it doesn’t. We cover 13 tested examples from basic generic classes all the way to real-world typed collection patterns.

If you’re comfortable with basic Groovy syntax and the def keyword and dynamic typing, you’re ready to go. If you want generics to be strictly enforced, you’ll also want to be familiar with @CompileStatic and @TypeChecked.

What Are Groovy Generics?

Generics let you write classes, interfaces, and methods that work with different types while still providing compile-time type safety. Instead of writing a separate Box class for every type – StringBox, IntegerBox, PersonBox – you write one Box<T> and let the caller decide what T is.

According to the official Groovy documentation, Groovy supports generic types in the same syntax as Java – angle brackets with type parameters. However, because Groovy is a dynamic language by default, generic type checks are only enforced at compile time when you use @CompileStatic or @TypeChecked.

Key Points:

  • Groovy generics use the same <T> syntax as Java generics
  • In dynamic mode, generics are advisory – the runtime won’t enforce them
  • With @CompileStatic, generics are enforced at compile time, just like Java
  • Type erasure still applies on the JVM – generic types are removed at runtime
  • Groovy adds convenience: the diamond operator, relaxed covariance, and dynamic dispatch all interact with generics

Why Use Generics in Groovy?

You might be thinking – if Groovy is dynamically typed and doesn’t enforce generics by default, why bother? Fair question. Here’s why generics still matter in Groovy code:

  • DocumentationList<String> tells the next developer exactly what’s in the list, even if the runtime doesn’t care
  • IDE support – IntelliJ IDEA and other IDEs use generic type information for autocompletion and refactoring
  • @CompileStatic safety – when you turn on static compilation, generics become real type contracts
  • Java interop – if your Groovy classes are consumed by Java code, generics provide proper type information
  • API design – generics let you create reusable, type-safe utilities and data structures

Think of generics in Groovy as a spectrum: at the relaxed end, they’re just helpful hints. At the strict end (with @CompileStatic), they’re fully enforced type contracts. You get to pick where you land.

Groovy Generics vs Java Generics

This is where things get interesting. If you’re coming from Java, you need to reset some expectations. Here’s a quick comparison:

FeatureJavaGroovy (dynamic)Groovy (@CompileStatic)
Generic type enforcementCompile timeNot enforcedCompile time
Adding wrong type to List<String>Compile errorSilently worksCompile error
Type erasure at runtimeYesYesYes
Diamond operatorYes (Java 7+)YesYes
Wildcards (? extends/super)YesYesYes
Reified genericsNoNoNo
Covariant return typesYesYes (more relaxed)Yes

The biggest difference: in plain Groovy, you can put anything into a List<String> without errors. Groovy’s dynamic dispatch simply ignores the generic type parameter at runtime. This is both a feature (flexibility) and a footgun (runtime surprises).

Syntax and Basic Usage

Generic Class Syntax

Here’s the basic syntax for defining a generic class in Groovy:

Generic Class Syntax

// Single type parameter
class Box<T> {
    T value
}

// Multiple type parameters
class Pair<K, V> {
    K key
    V value
}

// Bounded type parameter
class NumberBox<T extends Number> {
    T value
}

Generic Method Syntax

Generic Method Syntax

// Generic method with type parameter before return type
<T> T firstElement(List<T> list) {
    return list.isEmpty() ? null : list[0]
}

// Bounded generic method
<T extends Comparable<T>> T findMax(List<T> items) {
    return items.max()
}

Common Type Parameter Conventions

LetterConventionExample
TTypeBox<T>
EElementList<E>
KKeyMap<K, V>
VValueMap<K, V>
RReturn typeFunction<T, R>

13 Practical Examples

Example 1: Basic Generic Class

What we’re doing: Creating a simple generic Box class that holds a value of any type.

Example 1: Basic Generic Class

class Box<T> {
    T value

    Box(T value) {
        this.value = value
    }

    T getValue() {
        return value
    }

    String toString() {
        return "Box[${value}]"
    }
}

// Using with different types
def stringBox = new Box<String>("Hello Groovy")
def intBox = new Box<Integer>(42)
def listBox = new Box<List<String>>(["a", "b", "c"])

println stringBox
println intBox
println listBox
println "String value: ${stringBox.value}"
println "Int value: ${intBox.value}"
println "List value: ${listBox.value}"

Output

Box[Hello Groovy]
Box[42]
Box[[a, b, c]]
String value: Hello Groovy
Int value: 42
List value: [a, b, c]

What happened here: We defined a Box<T> class where T acts as a placeholder for any type. When we create new Box<String>("Hello Groovy"), the T becomes String for that instance. One class definition, unlimited type versatility.

Example 2: Generic Method

What we’re doing: Writing a standalone generic method that works with any type of list.

Example 2: Generic Method

// Generic method - the <T> before the return type declares the type parameter
<T> T firstOrDefault(List<T> list, T defaultValue) {
    return list.isEmpty() ? defaultValue : list[0]
}

<T> List<T> filterNulls(List<T> list) {
    return list.findAll { it != null }
}

// Using with strings
def names = ["Alice", "Bob", "Charlie"]
println "First name: ${firstOrDefault(names, 'Unknown')}"

// Using with numbers
def numbers = []
println "First number: ${firstOrDefault(numbers, -1)}"

// Filtering nulls
def mixed = ["Hello", null, "World", null, "Groovy"]
println "Filtered: ${filterNulls(mixed)}"

// Works with any type
def integers = [1, null, 3, null, 5]
println "Filtered ints: ${filterNulls(integers)}"

Output

First name: Alice
First number: -1
Filtered: [Hello, World, Groovy]
Filtered ints: [1, 3, 5]

What happened here: The <T> before the return type declares a type parameter for that method. The compiler (and IDE) can infer T from the arguments you pass. So firstOrDefault(names, 'Unknown') infers T = String automatically.

Example 3: Bounded Type Parameters (extends)

What we’re doing: Restricting a generic type to accept only Number and its subclasses using upper bounds.

Example 3: Bounded Type Parameters

class MathBox<T extends Number> {
    T value

    MathBox(T value) {
        this.value = value
    }

    double doubleValue() {
        return value.doubleValue() * 2
    }

    boolean isPositive() {
        return value.doubleValue() > 0
    }

    String toString() {
        return "MathBox[${value} (${value.getClass().simpleName})]"
    }
}

// Works with Integer, Double, BigDecimal - all extend Number
def intMath = new MathBox<Integer>(21)
def doubleMath = new MathBox<Double>(3.14)
def bigMath = new MathBox<BigDecimal>(99.99)

println intMath
println "Doubled: ${intMath.doubleValue()}"
println "Positive? ${intMath.isPositive()}"
println()
println doubleMath
println "Doubled: ${doubleMath.doubleValue()}"
println()
println bigMath
println "Doubled: ${bigMath.doubleValue()}"

// This would fail with @CompileStatic:
// def stringMath = new MathBox<String>("oops") // String doesn't extend Number

Output

MathBox[21 (Integer)]
Doubled: 42.0
Positive? true

MathBox[3.14 (BigDecimal)]
Doubled: 6.28

MathBox[99.99 (BigDecimal)]
Doubled: 199.98

What happened here: The <T extends Number> bound means T must be Number or any of its subclasses (Integer, Double, BigDecimal, etc.). This lets us safely call doubleValue() inside the class because we know T is always a Number.

Example 4: Generic Interface

What we’re doing: Defining a generic interface and implementing it with concrete types.

Example 4: Generic Interface

// Generic interface
interface Repository<T> {
    T findById(int id)
    List<T> findAll()
    void save(T entity)
}

// Concrete entity
class User {
    int id
    String name
    String toString() { "User(id=$id, name=$name)" }
}

// Implementation with concrete type
class UserRepository implements Repository<User> {
    private List<User> store = []

    User findById(int id) {
        return store.find { it.id == id }
    }

    List<User> findAll() {
        return store.asImmutable()
    }

    void save(User user) {
        store.removeAll { it.id == user.id }
        store.add(user)
    }
}

def repo = new UserRepository()
repo.save(new User(id: 1, name: "Alice"))
repo.save(new User(id: 2, name: "Bob"))
repo.save(new User(id: 3, name: "Charlie"))

println "All: ${repo.findAll()}"
println "Find #2: ${repo.findById(2)}"

// Update
repo.save(new User(id: 2, name: "Bob Updated"))
println "After update: ${repo.findById(2)}"

Output

All: [User(id=1, name=Alice), User(id=2, name=Bob), User(id=3, name=Charlie)]
Find #2: User(id=2, name=Bob)
After update: User(id=2, name=Bob Updated)

What happened here: The Repository<T> interface defines a contract for CRUD operations on any entity type. UserRepository implements Repository<User>, locking T to User. This pattern is the backbone of data access layers in Grails and Spring Boot.

Example 5: Multiple Type Parameters with Pair Class

What we’re doing: Building a generic Pair class that holds two values of different types.

Example 5: Multiple Type Parameters

class Pair<A, B> {
    final A first
    final B second

    Pair(A first, B second) {
        this.first = first
        this.second = second
    }

    // Generic method that swaps the pair
    Pair<B, A> swap() {
        return new Pair<B, A>(second, first)
    }

    // Transform both values using closures
    <C, D> Pair<C, D> map(Closure<C> mapFirst, Closure<D> mapSecond) {
        return new Pair<C, D>(mapFirst(first), mapSecond(second))
    }

    String toString() {
        return "(${first}, ${second})"
    }
}

// Different type combinations
def nameAge = new Pair<String, Integer>("Alice", 30)
println "Name-Age: ${nameAge}"
println "First: ${nameAge.first}, Second: ${nameAge.second}"

// Swap
def swapped = nameAge.swap()
println "Swapped: ${swapped}"

// Map/transform
def mapped = nameAge.map(
    { it.toUpperCase() },
    { it * 2 }
)
println "Mapped: ${mapped}"

// Pair of pairs
def nested = new Pair<String, Pair<Integer, Boolean>>(
    "config",
    new Pair<Integer, Boolean>(8080, true)
)
println "Nested: ${nested}"
println "Port: ${nested.second.first}"

Output

Name-Age: (Alice, 30)
First: Alice, Second: 30
Swapped: (30, Alice)
Mapped: (ALICE, 60)
Nested: (config, (8080, true))
Port: 8080

What happened here: Pair<A, B> uses two type parameters. The swap() method returns a Pair<B, A> with the types reversed. The map() method introduces its own type parameters <C, D> to transform both values into entirely new types.

Example 6: Wildcard Types (? extends and ? super)

What we’re doing: Using wildcard types for flexible method signatures that accept related types.

Example 6: Wildcard Types

// Upper-bounded wildcard: ? extends Number
// "Give me a list of anything that IS a Number"
double sumAll(List<? extends Number> numbers) {
    double total = 0
    numbers.each { total += it.doubleValue() }
    return total
}

// Lower-bounded wildcard: ? super Integer
// "Give me a list that CAN HOLD Integers"
void addNumbers(List<? super Integer> list) {
    list.add(10)
    list.add(20)
    list.add(30)
}

// Upper-bounded wildcard examples
List<Integer> ints = [1, 2, 3]
List<Double> doubles = [1.5, 2.5, 3.5]
List<BigDecimal> bigDecimals = [10.1, 20.2, 30.3]

println "Sum of ints: ${sumAll(ints)}"
println "Sum of doubles: ${sumAll(doubles)}"
println "Sum of BigDecimals: ${sumAll(bigDecimals)}"

// Lower-bounded wildcard example
List<Number> numberList = []
addNumbers(numberList)
println "Number list after addNumbers: ${numberList}"

List<Object> objectList = ["existing"]
addNumbers(objectList)
println "Object list after addNumbers: ${objectList}"

Output

Sum of ints: 6.0
Sum of doubles: 7.5
Sum of BigDecimals: 60.599999999999994
Number list after addNumbers: [10, 20, 30]
Object list after addNumbers: [existing, 10, 20, 30]

What happened here: The PECS principle applies – Producer extends, Consumer super. Use ? extends Number when you want to read numbers from a list (the list produces values). Use ? super Integer when you want to add integers to a list (the list consumes values).

Example 7: Generics with @CompileStatic

What we’re doing: Demonstrating how @CompileStatic enforces generic type safety at compile time.

Example 7: Generics with @CompileStatic

import groovy.transform.CompileStatic

// WITHOUT @CompileStatic - Groovy is relaxed about generics
List<String> dynamicList = []
dynamicList.add("Hello")
dynamicList.add(42)          // No error in dynamic mode!
dynamicList.add(true)        // Still no error!
println "Dynamic list: ${dynamicList}"
println "Types: ${dynamicList.collect { it.getClass().simpleName }}"

// WITH @CompileStatic - strict enforcement
@CompileStatic
List<String> buildSafeList() {
    List<String> safeList = []
    safeList.add("Hello")
    safeList.add("World")
    // safeList.add(42)       // COMPILE ERROR: Cannot call add(String) with int
    // safeList.add(true)     // COMPILE ERROR: Cannot call add(String) with boolean
    return safeList
}

def safe = buildSafeList()
println "Safe list: ${safe}"
println "Types: ${safe.collect { it.getClass().simpleName }}"

// @CompileStatic also enforces return types
@CompileStatic
String getFirstName(List<String> names) {
    return names.isEmpty() ? "Unknown" : names[0]
    // If list were List<Integer>, this would fail - return type mismatch
}

println "First: ${getFirstName(["Alice", "Bob"])}"

Output

Dynamic list: [Hello, 42, true]
Types: [String, Integer, Boolean]
Safe list: [Hello, World]
Types: [String, String]
First: Alice

What happened here: In dynamic Groovy, we added an Integer and a Boolean to a List<String> without any error. With @CompileStatic, those same lines would cause compile errors. This is the core difference between Groovy’s relaxed generics and strict enforcement. For more on this, see our @CompileStatic and @TypeChecked guide.

Example 8: Type Erasure Behavior

What we’re doing: Proving that generic types are erased at runtime on the JVM – you can’t check the generic type of a collection at runtime.

Example 8: Type Erasure

// Create typed lists
List<String> strings = ["Hello", "World"]
List<Integer> integers = [1, 2, 3]
List<Boolean> booleans = [true, false]

// At runtime, they're ALL just ArrayList - the generic type is erased
println "strings class:  ${strings.getClass().name}"
println "integers class: ${integers.getClass().name}"
println "booleans class: ${booleans.getClass().name}"

// You CANNOT distinguish between List<String> and List<Integer> at runtime
println "Same class? ${strings.getClass() == integers.getClass()}"

// instanceof doesn't know about generics either
println "strings instanceof List: ${strings instanceof List}"
// There's no way to write: strings instanceof List<String>

// Generic class type erasure
class Container<T> {
    T item
    Container(T item) { this.item = item }

    // This method can't check T at runtime
    String getTypeName() {
        return item?.getClass()?.simpleName ?: "null"
    }
}

def strContainer = new Container<String>("test")
def intContainer = new Container<Integer>(42)

println "String container class: ${strContainer.getClass().name}"
println "Int container class: ${intContainer.getClass().name}"
println "Same class? ${strContainer.getClass() == intContainer.getClass()}"
println "Item type (str): ${strContainer.getTypeName()}"
println "Item type (int): ${intContainer.getTypeName()}"

Output

strings class:  java.util.ArrayList
integers class: java.util.ArrayList
booleans class: java.util.ArrayList
Same class? true
strings instanceof List: true
String container class: Container
Int container class: Container
Same class? true
Item type (str): String
Item type (int): Integer

What happened here: Type erasure means that List<String> and List<Integer> are the exact same class at runtime – just ArrayList. The JVM has no knowledge of the generic type parameter after compilation. This is why you can’t use instanceof to check generic types and why Groovy (and Java) don’t have reified generics.

Example 9: Covariance with Collections

What we’re doing: Exploring how Groovy handles covariant and contravariant collection assignments differently from Java.

Example 9: Covariance with Collections

// In Java, this would be a compile error:
// List<Number> nums = new ArrayList<Integer>();
// But in dynamic Groovy, it works just fine!

List<Integer> integers = [1, 2, 3]
List<Number> numbers = integers  // Covariant assignment - works in Groovy
println "Numbers: ${numbers}"

// Even more relaxed - Groovy lets you assign across unrelated generic types
List<String> strings = ["Hello", "World"]
List<Object> objects = strings
println "Objects: ${objects}"

// This is useful for method parameters
void printNumbers(List<Number> nums) {
    nums.each { println "  ${it} (${it.getClass().simpleName})" }
}

println "Printing integers as numbers:"
printNumbers([1, 2, 3])

println "Printing mixed numbers:"
printNumbers([1, 2.5, 3L, 4.0f])

// Demonstrating covariant return types
class Animal {
    String name
    Animal(String name) { this.name = name }
    String toString() { "${this.getClass().simpleName}($name)" }
}

class Dog extends Animal {
    Dog(String name) { super(name) }
}

class AnimalFactory {
    Animal create(String name) { new Animal(name) }
}

class DogFactory extends AnimalFactory {
    // Covariant return type - returns Dog instead of Animal
    Dog create(String name) { new Dog(name) }
}

def factory = new DogFactory()
Animal pet = factory.create("Buddy")
println "Created: ${pet}"
println "Is Dog? ${pet instanceof Dog}"

Output

Numbers: [1, 2, 3]
Objects: [Hello, World]
Printing integers as numbers:
  1 (Integer)
  2 (Integer)
  3 (Integer)
Printing mixed numbers:
  1 (Integer)
  2.5 (BigDecimal)
  3 (Long)
  4.0 (Float)
Created: Dog(Buddy)
Is Dog? true

What happened here: Groovy’s dynamic nature makes it more permissive with generic type assignments than Java. You can assign a List<Integer> to a List<Number> without wildcards. Groovy also supports covariant return types – a subclass method can return a more specific type than the parent class declares.

Example 10: Generic Closures

What we’re doing: Using generics with closures for type-safe functional patterns.

Example 10: Generic Closures

// Closure with typed parameters
Closure<String> formatter = { String name, int age ->
    return "Name: ${name}, Age: ${age}"
}
println formatter("Alice", 30)

// Generic method in a helper class
class CollectionUtils {
    static <T, R> List<R> transform(List<T> input, Closure<R> transformer) {
        return input.collect { transformer(it) }
    }
}

// Transform strings to their lengths
List<String> words = ["Groovy", "is", "awesome"]
List<Integer> lengths = CollectionUtils.transform(words) { String s -> s.length() }
println "Words: ${words}"
println "Lengths: ${lengths}"

// Transform numbers to formatted strings
List<Integer> scores = [95, 87, 72, 100]
List<String> formatted = CollectionUtils.transform(scores) { int s -> "${s}%" }
println "Scores: ${formatted}"

// Closure that returns a closure - generic factory pattern
def defaultValueProvider(defaultValue) {
    return { value -> value ?: defaultValue }
}

def stringDefault = defaultValueProvider("N/A")
def intDefault = defaultValueProvider(0)

println "With value: ${stringDefault('Hello')}"
println "Without value: ${stringDefault(null)}"
println "Int with value: ${intDefault(42)}"
println "Int without value: ${intDefault(null)}"

Output

Name: Alice, Age: 30
Words: [Groovy, is, awesome]
Lengths: [6, 2, 7]
Scores: [95%, 87%, 72%, 100%]
With value: Hello
Without value: N/A
Int with value: 42
Int without value: 0

What happened here: Groovy’s Closure<R> type parameter represents the return type of the closure. We combined this with generic methods to create a transform() function that converts a list of one type into a list of another type. The closure factory pattern at the end shows how generics and closures compose beautifully in Groovy.

Example 11: Diamond Operator

What we’re doing: Using the diamond operator <> for concise generic instantiation.

Example 11: Diamond Operator

// Without diamond - verbose
Map<String, List<Integer>> verbose = new HashMap<String, List<Integer>>()

// With diamond - the compiler infers the types from the left side
Map<String, List<Integer>> concise = new HashMap<>()

// Works with all generic types
List<String> names = new ArrayList<>()
names.add("Alice")
names.add("Bob")

Set<Integer> uniqueNums = new LinkedHashSet<>()
uniqueNums.addAll([1, 2, 3, 2, 1])

Map<String, Integer> scores = new LinkedHashMap<>()
scores.put("Alice", 95)
scores.put("Bob", 87)

println "Names: ${names}"
println "Unique nums: ${uniqueNums}"
println "Scores: ${scores}"

// Diamond with custom generic classes
class Stack<E> {
    private List<E> elements = []

    void push(E item) { elements.add(item) }
    E pop() { elements.isEmpty() ? null : elements.remove(elements.size() - 1) }
    E peek() { elements.isEmpty() ? null : elements.last() }
    int size() { elements.size() }
    String toString() { "Stack${elements}" }
}

Stack<String> stack = new Stack<>()
stack.push("first")
stack.push("second")
stack.push("third")
println "Stack: ${stack}"
println "Pop: ${stack.pop()}"
println "Peek: ${stack.peek()}"
println "After pop: ${stack}"

// Groovy shorthand - even simpler
def listOfMaps = [] as List<Map<String, Object>>
listOfMaps.add([name: "Alice", age: 30])
listOfMaps.add([name: "Bob", age: 25])
println "List of maps: ${listOfMaps}"

Output

Names: [Alice, Bob]
Unique nums: [1, 2, 3]
Scores: [Alice:95, Bob:87]
Stack: Stack[first, second, third]
Pop: third
Peek: second
After pop: Stack[first, second]
List of maps: [[name:Alice, age:30], [name:Bob, age:25]]

What happened here: The diamond operator <> avoids repeating the type parameters on the right side of an assignment. Groovy infers them from the declaration on the left. This makes complex nested generics like Map<String, List<Integer>> much less noisy to instantiate.

Example 12: Groovy’s Relaxed Generics vs Java’s Strict Generics

What we’re doing: Showing the full spectrum of what dynamic Groovy lets you get away with – things that would never compile in Java.

Example 12: Relaxed vs Strict Generics

// 1. Adding wrong type to a typed list - Groovy doesn't care in dynamic mode
List<String> strings = []
strings.add("Hello")
strings.add(42)           // In Java: COMPILE ERROR - in Groovy: works fine
strings.add([1, 2, 3])   // Adding a list into a List<String> - still works
println "Mixed 'String' list: ${strings}"
println "Types: ${strings.collect { it.getClass().simpleName }}"

// 2. Assigning incompatible generic types
List<Integer> ints = ["not", "integers", "at", "all"]  // No error!
println "Integer list with strings: ${ints}"

// 3. Generic method ignoring type constraints (in a helper class)
class MathHelper {
    static <T extends Number> T addOne(T value) {
        return value + 1
    }

    // 4. Return type generics are ignored dynamically
    static <T> T gimmeAnything() {
        return "I'm a String, not T"
    }
}

println "addOne(5): ${MathHelper.addOne(5)}"
println "addOne(3.14): ${MathHelper.addOne(3.14)}"
// In dynamic mode, you could even call addOne("hello") - Groovy would try
// to execute "hello" + 1, which would either fail at runtime or produce
// unexpected results depending on the Groovy version

// 4. (continued) Return type generics - casting behavior
String s = MathHelper.gimmeAnything()
println "s = ${s} (${s.getClass().simpleName})"
// Integer i = MathHelper.gimmeAnything() - throws GroovyCastException at runtime!
try {
    Integer i = MathHelper.gimmeAnything()
    println "i = ${i} (${i.getClass().simpleName})"
} catch (Exception e) {
    println "Integer cast failed: ${e.class.simpleName} - Groovy enforces the cast!"
}

// 5. The def keyword makes generics completely optional
def flexibleList = ["mixed", 42, true, 3.14]
println "Flexible: ${flexibleList}"
println "Types: ${flexibleList.collect { it.getClass().simpleName }}"

Output

Mixed 'String' list: [Hello, 42, [1, 2, 3]]
Types: [String, Integer, ArrayList]
Integer list with strings: [not, integers, at, all]
addOne(5): 6
addOne(3.14): 4.14
s = I'm a String, not T (String)
Integer cast failed: GroovyCastException - Groovy enforces the cast!
Flexible: [mixed, 42, true, 3.14]
Types: [String, Integer, Boolean, BigDecimal]

What happened here: This is the wild side of Groovy generics. Dynamic Groovy treats generic type parameters as suggestions, not rules. You can put strings into a List<Integer>, return the wrong type from a generic method, and assign incompatible types – all without errors. This is because Groovy’s dynamic dispatch happens at runtime, where type erasure has already removed the generic information. If you need enforcement, use @CompileStatic. Read more about this in our def keyword and dynamic typing guide.

Example 13: Real-World Typed Collection Patterns

What we’re doing: Building practical, real-world generic utilities that you’d actually use in production Groovy code.

Example 13: Real-World Typed Collection Patterns

import groovy.transform.CompileStatic

// Pattern 1: Type-safe result wrapper
class Result<T> {
    final T value
    final String error
    final boolean success

    private Result(T value, String error, boolean success) {
        this.value = value
        this.error = error
        this.success = success
    }

    static <T> Result<T> ok(T value) {
        return new Result<T>(value, null, true)
    }

    static <T> Result<T> fail(String error) {
        return new Result<T>(null, error, false)
    }

    String toString() {
        success ? "Result.OK(${value})" : "Result.FAIL(${error})"
    }
}

// Pattern 2: Type-safe registry/cache
class TypedCache<K, V> {
    private final Map<K, V> cache = new LinkedHashMap<>()
    private final Closure<V> loader

    TypedCache(Closure<V> loader) {
        this.loader = loader
    }

    V get(K key) {
        if (!cache.containsKey(key)) {
            cache[key] = loader(key)
        }
        return cache[key]
    }

    int size() { cache.size() }
    String toString() { "Cache${cache}" }
}

// Pattern 3: Type-safe event system
class EventBus<E> {
    private List<Closure> listeners = []

    void on(Closure listener) { listeners.add(listener) }

    void emit(E event) {
        listeners.each { it(event) }
    }
}

// Using Result
Result<Integer> parseAge(String input) {
    try {
        int age = Integer.parseInt(input)
        return age >= 0 ? Result.ok(age) : Result.fail("Age must be positive")
    } catch (NumberFormatException e) {
        return Result.fail("Invalid number: ${input}")
    }
}

println parseAge("25")
println parseAge("-5")
println parseAge("abc")

// Using TypedCache
def userCache = new TypedCache<Integer, String>({ Integer id ->
    println "  (Loading user ${id} from database...)"
    return "User_${id}"
})

println "\nFirst access:"
println "User 1: ${userCache.get(1)}"
println "User 2: ${userCache.get(2)}"
println "\nSecond access (cached):"
println "User 1: ${userCache.get(1)}"
println "Cache: ${userCache}"

// Using EventBus
def bus = new EventBus<Map<String, Object>>()
bus.on { event -> println "Logger: ${event}" }
bus.on { event -> println "Analytics: tracking ${event.action}" }

println "\nEmitting events:"
bus.emit([action: "login", user: "Alice"])
bus.emit([action: "purchase", user: "Bob", amount: 49.99])

Output

Result.OK(25)
Result.FAIL(Age must be positive)
Result.FAIL(Invalid number: abc)

First access:
  (Loading user 1 from database...)
User 1: User_1
  (Loading user 2 from database...)
User 2: User_2

Second access (cached):
User 1: User_1
Cache: Cache[1:User_1, 2:User_2]

Emitting events:
Logger: [action:login, user:Alice]
Analytics: tracking login
Logger: [action:purchase, user:Bob, amount:49.99]
Analytics: tracking purchase

What happened here: These are patterns you’ll use in real projects. The Result<T> wrapper eliminates null-checking and exception-throwing for operations that can fail. The TypedCache<K, V> provides a lazy-loading cache with generics. The EventBus<E> creates a typed publish-subscribe system. All three use Groovy generics to make the API self-documenting and type-safe.

Type Erasure and Limitations

One of the most common sources of confusion with Groovy generics is type erasure. Since Groovy runs on the JVM, it inherits Java’s type erasure model. Here’s what that means in practice:

  • No runtime type checking – you can’t write if (list instanceof List<String>) because the generic type is gone at runtime
  • No generic array creationnew T[10] isn’t possible because the JVM doesn’t know what T is
  • No reified generics – unlike Kotlin’s reified keyword, Groovy has no way to access generic type information at runtime
  • Method overloading limitations – you can’t have both void process(List<String> items) and void process(List<Integer> items) because after erasure they both become void process(List items)

The workaround for runtime type information is to pass the Class object explicitly:

Workaround: Passing Class Object

// Workaround for reified generics - pass the Class explicitly
def createInstance(Class type) {
    return type.getDeclaredConstructor().newInstance()
}

def str = createInstance(String)
def list = createInstance(ArrayList)
println "Created: ${str.getClass().simpleName} -> '${str}'"
println "Created: ${list.getClass().simpleName} -> ${list}"

// Type-safe deserialization pattern
def parseAll(List<Map> rawData, Class type) {
    return rawData.collect { map ->
        def instance = type.getDeclaredConstructor().newInstance()
        map.each { k, v -> instance[k] = v }
        return instance
    }
}

Output

Created: String -> ''
Created: ArrayList -> []

Edge Cases and Best Practices

Best Practices

DO:

  • Use generics for documentation, even in dynamic Groovy – List<String> is clearer than List
  • Use @CompileStatic on methods that need type safety – especially in libraries and shared utilities
  • Use bounded types (<T extends Number>) when you need access to specific methods
  • Prefer the diamond operator <> for cleaner instantiation
  • Pass Class<T> objects when you need runtime type information

DON’T:

  • Rely on generics for runtime type safety in dynamic Groovy – use @CompileStatic for that
  • Try to use instanceof with generic types – it doesn’t work due to type erasure
  • Overuse wildcards – if a simple type parameter works, prefer that over ? extends or ? super
  • Assume Groovy generics behave exactly like Java generics – the dynamic nature changes everything
  • Create deeply nested generic types like Map<String, List<Pair<Integer, Map<String, Object>>>> – use type aliases or wrapper classes instead

Performance Considerations

Generics themselves have zero runtime performance cost thanks to type erasure – the JVM treats List<String> and raw List identically. However, there are a few related performance points to keep in mind:

  • @CompileStatic with generics – when you use @CompileStatic with typed collections, the compiler generates direct method calls instead of going through Groovy’s Meta-Object Protocol (MOP). This can be 2-5x faster for tight loops over typed collections
  • Boxing with primitive typesList<Integer> stores boxed Integer objects, not primitive int values. For large numeric collections, consider using int[] arrays instead
  • Type casting overhead – in dynamic Groovy, the runtime inserts casts when reading from generic collections. With @CompileStatic, these casts are optimized by the compiler
  • No impact on memory – generics don’t add any memory overhead since type parameters are erased. A Box<String> and Box<Integer> use the same amount of memory for the container itself

Common Pitfalls

Pitfall 1: Assuming Dynamic Groovy Enforces Generics

Problem: Declaring List<String> and expecting it to reject non-String values.

Pitfall 1: False Type Safety

// You think this is safe...
List<String> names = []
names.add("Alice")
names.add(42)       // No error! This silently works in dynamic Groovy

// Later, when you try to use it as a String:
names.each { name ->
    // println name.toUpperCase()  // RUNTIME ERROR on 42!
    println "${name} -> ${name.getClass().simpleName}"
}

Output

Alice -> String
42 -> Integer

Solution: If you need type enforcement, annotate your method or class with @CompileStatic. Otherwise, treat generics as documentation, not guarantees.

Pitfall 2: Method Overloading with Erased Types

Problem: Trying to overload methods that differ only in generic type parameters.

Pitfall 2: Overloading Clash

// These two methods have the SAME erasure: process(List)
// void process(List<String> items) { ... }
// void process(List<Integer> items) { ... }  // ERROR: same erasure

// Solution: use different method names or a single generic method
<T> void process(List<T> items, Class<T> type) {
    println "Processing ${items.size()} items of type ${type.simpleName}"
    items.each { println "  ${it}" }
}

process(["A", "B", "C"], String)
process([1, 2, 3], Integer)

Output

Processing 3 items of type String
  A
  B
  C
Processing 3 items of type Integer
  1
  2
  3

Solution: Use different method names, a single generic method, or pass the Class<T> as an additional parameter to differentiate behavior at runtime.

Pitfall 3: Mixing def with Generics

Problem: Using def on the left side and a generic type on the right – the generic information is lost for IDE support.

Pitfall 3: def Erases Generic Info for IDEs

// BAD - IDE loses type information
def names = new ArrayList<String>()
// IDE doesn't know names is List<String>, treats it as Object

// GOOD - preserve generic type on the left
List<String> names2 = new ArrayList<>()
// IDE knows names2 is List<String> - full autocomplete works

// ALSO GOOD - Groovy literal syntax preserves type
List<String> names3 = ["Alice", "Bob"]

println "names: ${names}"
println "names2: ${names2}"
println "names3: ${names3}"

Output

names: []
names2: []
names3: [Alice, Bob]

Solution: When using generics, declare the type explicitly on the left side instead of using def. Your IDE will thank you with better autocomplete and error detection.

Conclusion

Groovy generics sit at an interesting intersection between Java’s strict type system and Groovy’s dynamic nature. In plain Groovy, they serve as documentation and IDE hints – the runtime won’t enforce them. With @CompileStatic, they become fully enforced type contracts identical to Java. Either way, understanding generics makes your Groovy code clearer, safer, and more interoperable with Java.

The most important thing to remember: Groovy won’t stop you from putting an Integer into a List<String> in dynamic mode. This isn’t a bug – it’s a design choice that favors flexibility. If you need strict enforcement, @CompileStatic is your friend. If you’re building APIs consumed by other developers (Groovy or Java), always use proper generic type parameters.

Now that you understand generics, you’re well-equipped to write type-safe Groovy code that plays nicely with Java libraries. For more on Groovy’s type system, check out our guides on @CompileStatic and @TypeChecked and Groovy’s def keyword and dynamic typing.

Summary

  • Groovy generics use the same <T> syntax as Java but are not enforced in dynamic mode
  • Use @CompileStatic when you need compile-time generic type enforcement
  • Bounded types (<T extends Number>) let you access specific methods while keeping the code generic
  • Type erasure means generic types are gone at runtime – no instanceof checks on generic parameters
  • The diamond operator <> reduces verbosity when creating generic instances
  • Use PECS (Producer extends, Consumer super) for wildcard type parameters
  • Prefer explicit type declarations over def when using generics for better IDE support

If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.

Up next: Groovy Annotations – Built-in and Custom

Frequently Asked Questions

Do Groovy generics work the same as Java generics?

Not exactly. Groovy uses the same <T> syntax as Java, but in dynamic Groovy (without @CompileStatic), generic type parameters are not enforced. You can put any type into a List<String> without errors. With @CompileStatic, Groovy enforces generics at compile time just like Java. Type erasure applies in both languages since they both run on the JVM.

Why can I add an Integer to a List<String> in Groovy?

Because dynamic Groovy does not enforce generic type parameters at runtime. Due to JVM type erasure, a List<String> is just a List at runtime, so any object can be added. This is by design – Groovy prioritizes flexibility and duck typing. If you need strict type checking, annotate your code with @CompileStatic to get compile-time enforcement.

What is the diamond operator in Groovy?

The diamond operator <> lets you omit repeated type parameters on the right side of a generic instantiation. Instead of writing Map<String, List<Integer>> map = new HashMap<String, List<Integer>>(), you write Map<String, List<Integer>> map = new HashMap<>(). The compiler infers the type parameters from the left-hand side declaration.

Does Groovy support reified generics?

No. Like Java, Groovy runs on the JVM which uses type erasure – generic type information is removed at runtime. You cannot check instanceof List<String> or create instances of type parameters (new T()). The workaround is to pass a Class<T> parameter explicitly when you need runtime type information.

Should I use generics in Groovy scripts or only in classes?

You should use generics wherever they add clarity, even in scripts. While dynamic Groovy won’t enforce them, generics serve as documentation for other developers and improve IDE support (autocompletion, refactoring). In production code, combine generics with @CompileStatic for maximum type safety. In quick scripts, generics are optional but still helpful for readability.

Previous in Series: Groovy Process Execution and System Commands

Next in Series: Groovy Annotations – Built-in and Custom

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *