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.
Table of Contents
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:
- Documentation –
List<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:
| Feature | Java | Groovy (dynamic) | Groovy (@CompileStatic) |
|---|---|---|---|
| Generic type enforcement | Compile time | Not enforced | Compile time |
| Adding wrong type to List<String> | Compile error | Silently works | Compile error |
| Type erasure at runtime | Yes | Yes | Yes |
| Diamond operator | Yes (Java 7+) | Yes | Yes |
| Wildcards (? extends/super) | Yes | Yes | Yes |
| Reified generics | No | No | No |
| Covariant return types | Yes | Yes (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
| Letter | Convention | Example |
|---|---|---|
T | Type | Box<T> |
E | Element | List<E> |
K | Key | Map<K, V> |
V | Value | Map<K, V> |
R | Return type | Function<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 creation –
new T[10]isn’t possible because the JVM doesn’t know whatTis - No reified generics – unlike Kotlin’s
reifiedkeyword, Groovy has no way to access generic type information at runtime - Method overloading limitations – you can’t have both
void process(List<String> items)andvoid process(List<Integer> items)because after erasure they both becomevoid 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 thanList - Use
@CompileStaticon 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
@CompileStaticfor that - Try to use
instanceofwith generic types – it doesn’t work due to type erasure - Overuse wildcards – if a simple type parameter works, prefer that over
? extendsor? 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
@CompileStaticwith 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 types –
List<Integer>stores boxedIntegerobjects, not primitiveintvalues. For large numeric collections, consider usingint[]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>andBox<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
@CompileStaticwhen 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
instanceofchecks on generic parameters - The diamond operator
<>reduces verbosity when creating generic instances - Use PECS (Producer
extends, Consumersuper) for wildcard type parameters - Prefer explicit type declarations over
defwhen 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.
Related Posts
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

No comment