The Groovy def keyword is covered here with 12 practical examples. Learn dynamic typing, type inference, and when to use def vs explicit types. Tested on Groovy 5.x.
“The best code is the code you don’t have to write. Groovy’s def keyword is proof of that.”
Dierk König, Groovy in Action
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner | Reading Time: 14 minutes
If you’ve looked at any Groovy code, you’ve probably seen the Groovy def keyword everywhere. It’s one of the first things that stands out when coming from Java – instead of declaring String name = "John", Groovy developers just write def name = "John". But what does def actually do? And when should you use it versus specifying the type explicitly?
This post explains exactly what the def keyword does, how Groovy dynamic typing works under the hood, and when it makes sense to use def versus explicit type declarations. We’ll look at 12 practical examples covering variables, methods, closures, and some gotchas that trip up even experienced developers.
If you haven’t set up Groovy yet, check out our Groovy Hello World guide first to get up and running.
Table of Contents
What is the Groovy Def Keyword?
The def keyword in Groovy is a way to declare a variable, method, or property without specifying its type. It tells Groovy: “I don’t care about the type right now – figure it out at runtime.”
Behind the scenes, def is essentially an alias for Object. When you write def x = 42, the compiled bytecode treats x as type Object, but Groovy’s runtime knows it’s an Integer and handles method dispatch accordingly.
According to the official Groovy documentation, “def is a replacement for a type name. It can be used to indicate that you don’t care about the type of a variable.”
Key Points:
defmakes a variable dynamically typed – the type is resolved at runtime- Under the hood,
defcompiles toObject - A variable declared with
defcan hold any type and change types at any time - You can still use explicit types like
String,int,List– Groovy is optionally typed - In scripts, you can even skip
defentirely – bare assignments create binding variables
Why Use Def in Groovy?
The main appeal of def is simplicity. When the type is obvious from the right-hand side of an assignment, declaring it again on the left feels redundant. Compare these:
Explicit vs Def
// Java-style explicit types String name = "Alice" Integer age = 30 List<String> cities = ["London", "Tokyo", "Paris"] Map<String, Integer> scores = [math: 95, english: 88] // Groovy def - cleaner, same result def name = "Alice" def age = 30 def cities = ["London", "Tokyo", "Paris"] def scores = [math: 95, english: 88]
Both versions do the exact same thing. The def version is shorter and easier to read – you see what the variable is from the value, not the type declaration.
Other benefits of using def:
- Flexibility: Variables can change types if needed (though this should be rare)
- Less boilerplate: No need to import or repeat complex generic types
- Faster prototyping: Focus on logic, not type declarations
- Duck typing: “If it walks like a duck and quacks like a duck, it’s a duck” – Groovy cares about what an object can do, not what type it is
When to Use Def vs Explicit Types
This is where it gets practical. Here’s a simple guide:
| Scenario | Use | Why |
|---|---|---|
| Local variables with obvious values | def | Type is clear from the right side |
| Method parameters in APIs | Explicit type | Documents what callers should pass |
| Method return types in APIs | Explicit type | Documents what callers get back |
| Scripts and quick prototypes | def | Speed over formality |
| Performance-critical code | Explicit type + @CompileStatic | Enables compile-time optimization |
| Domain class properties | Explicit type | GORM needs types for mapping |
The rule of thumb: use def for local variables and private internals. Use explicit types for public APIs, method signatures, and anything other developers will call.
How Def Works Under the Hood
When you use def, Groovy doesn’t just ignore types. It uses a mechanism called dynamic dispatch through the Meta-Object Protocol (MOP). Here’s the process:
- Compilation:
defvariables compile to typeObjectin the bytecode - Assignment: The actual value gets stored with its real type (Integer, String, etc.)
- Method calls: Groovy’s runtime looks up the actual type and dispatches the right method
- Type checking: Happens at runtime, not compile time – so type errors show up when you run the code
Def Under the Hood
def x = 42 println x.getClass().name // java.lang.Integer x = "now I'm a String" println x.getClass().name // java.lang.String x = [1, 2, 3] println x.getClass().name // java.util.ArrayList
Output
java.lang.Integer java.lang.String java.util.ArrayList
The variable x started as an Integer, became a String, then became a List – all in the same variable. The type declaration is Object, but the actual runtime type changes with each assignment.
Syntax and Basic Usage
Variable Declaration
Def Syntax
// Variable declaration with def
def variableName = value
// Method declaration with def return type
def methodName(parameters) {
// body
}
// Closure with def
def closureName = { params -> body }
Def vs No Def in Scripts
There’s a subtle but important difference in Groovy scripts:
Def vs No Def in Scripts
// With def - local variable, scoped to the script
def localVar = "I'm local"
// Without def - binding variable, accessible from methods
globalVar = "I'm in the binding"
def showVars() {
// println localVar // ERROR - can't access local var
println globalVar // OK - binding variable
}
showVars()
Output
I'm in the binding
In scripts, def creates a local variable. Without def, the variable goes into the script’s Binding object, making it accessible from methods defined in the same script.
12 Practical Def Keyword Examples
Example 1: Basic Variable Declaration
What we’re doing: Declaring variables of different types with def.
Example 1: Basic Variables
def name = "Alice"
def age = 28
def salary = 75000.50
def active = true
println "Name: ${name} (${name.getClass().simpleName})"
println "Age: ${age} (${age.getClass().simpleName})"
println "Salary: ${salary} (${salary.getClass().simpleName})"
println "Active: ${active} (${active.getClass().simpleName})"
Output
Name: Alice (String) Age: 28 (Integer) Salary: 75000.50 (BigDecimal) Active: true (Boolean)
What happened here: Even though we used def for all variables, Groovy assigned the correct type based on the value. Notice how 75000.50 became a BigDecimal, not a Double – Groovy uses BigDecimal by default for decimal literals to avoid floating-point precision issues.
Example 2: Type Reassignment
What we’re doing: Showing how a def variable can change types.
Example 2: Type Reassignment
def value = 100
println "Start: ${value} → ${value.getClass().simpleName}"
value = "hundred"
println "After: ${value} → ${value.getClass().simpleName}"
value = [1, 2, 3]
println "Final: ${value} → ${value.getClass().simpleName}"
Output
Start: 100 → Integer After: hundred → String Final: [1, 2, 3] → ArrayList
What happened here: The same variable changed from Integer to String to ArrayList. This is possible because def compiles to Object, which can hold anything. With an explicit type like int value = 100, the reassignment to a String would fail at compile time.
Example 3: Def with Collections
What we’re doing: Using def with lists and maps.
Example 3: Collections
def fruits = ["apple", "banana", "cherry"]
def person = [name: "Bob", age: 25, city: "London"]
println "Fruits: ${fruits}"
println "First fruit: ${fruits[0]}"
println "Person: ${person}"
println "Name: ${person.name}"
Output
Fruits: [apple, banana, cherry] First fruit: apple Person: [name:Bob, age:25, city:London] Name: Bob
What happened here: def works naturally with Groovy’s list and map literals. You get full access to all collection methods – each(), collect(), findAll() – without ever specifying the type.
Example 4: Def in Method Return Types
What we’re doing: Using def as a method return type.
Example 4: Method Return Type
def getGreeting(String name) {
return "Hello, ${name}!"
}
def getAge() {
return 30
}
def getConfig() {
return [debug: true, port: 8080]
}
println getGreeting("Alice")
println "Age: ${getAge()}"
println "Config: ${getConfig()}"
Output
Hello, Alice! Age: 30 Config: [debug:true, port:8080]
What happened here: Each method returns a different type – String, Integer, and Map – but they all use def as the return type. Groovy figures out the actual type from what’s returned. In scripts and internal code, this is perfectly fine. For public APIs, consider using explicit return types for documentation.
Example 5: Def in For-Each Loops
What we’re doing: Using def in loop variable declarations.
Example 5: Loop Variables
def numbers = [10, 20, 30, 40, 50]
// Using def in a for loop
for (def num in numbers) {
print "${num} "
}
println()
// More idiomatic - just use the type directly
for (int num in numbers) {
print "${num * 2} "
}
println()
// Most idiomatic - use each closure
numbers.each { num ->
print "${num + 1} "
}
println()
Output
10 20 30 40 50 20 40 60 80 100 11 21 31 41 51
What happened here: You can use def in for-each loops, though it’s optional. In each() closures, you don’t even need def – the closure parameter is implicitly dynamic.
Example 6: Def vs Explicit Type – What Changes
What we’re doing: Comparing behavior of def vs explicit type when reassigning.
Example 6: Def vs Typed
// With def - flexible
def flexible = "hello"
flexible = 42 // OK - def allows type change
println "Flexible: ${flexible}"
// With explicit type - strict
String strict = "hello"
try {
strict = 42 // Groovy will try to cast
println "Strict: ${strict}"
} catch (Exception e) {
println "Error: ${e.message}"
}
Output
Flexible: 42 Strict: 42
What happened here: Surprise – both work! When you assign an integer to a typed String variable, Groovy automatically converts it using toString(). However, with @CompileStatic enabled, the explicit type version would fail at compile time. That’s the difference between dynamic and static modes.
Example 7: Multiple Variable Declaration
What we’re doing: Declaring multiple variables using def.
Example 7: Multiple Variables
// Multiple assignments using destructuring
def (name, age, city) = ["Emma", 32, "Paris"]
println "Name: ${name}"
println "Age: ${age}"
println "City: ${city}"
Output
Name: Emma Age: 32 City: Paris
What happened here: Groovy supports multiple assignment (destructuring) with def. The list on the right gets unpacked into the variables on the left. This is a concise way to extract values from a list or a method that returns multiple values.
Example 8: Def with Null and Null Safety
What we’re doing: Handling null values with def.
Example 8: Null Handling
def value = null
println "Value: ${value}"
println "Is null: ${value == null}"
println "Type: ${value?.getClass()?.simpleName ?: 'null'}"
// Safe navigation with def variables
def user = null
println "User name: ${user?.name ?: 'Unknown'}"
// Assign a value
user = [name: "Charlie", email: "charlie@example.com"]
println "User name: ${user?.name ?: 'Unknown'}"
Output
Value: null Is null: true Type: null User name: Unknown User name: Charlie
What happened here: def variables can hold null. Groovy’s safe navigation operator (?.) and Elvis operator (?:) work perfectly with dynamically typed variables. No NullPointerException here.
Example 9: Def in Class Properties
What we’re doing: Using def in class property declarations.
Example 9: Class Properties
class Product {
def name
def price
def category
String toString() {
"${name} - \$${price} (${category})"
}
}
def laptop = new Product(name: "MacBook", price: 1299.99, category: "Electronics")
println laptop
def book = new Product(name: "Groovy in Action", price: 49.99, category: "Books")
println book
Output
MacBook - $1299.99 (Electronics) Groovy in Action - $49.99 (Books)
What happened here: Class properties can use def too. Groovy auto-generates getters and setters, and the map constructor works for setting properties. For production domain classes though, explicit types are recommended – especially with GORM, which needs types for database column mapping.
Pro Tip: In Groovy classes, properties declared without an access modifier (
def nameorString name) automatically get public getter and setter methods. You don’t need to write them.
Example 10: Def with Closures
What we’re doing: Storing closures in def variables.
Example 10: Def with Closures
def add = { a, b -> a + b }
def multiply = { a, b -> a * b }
def greet = { name -> "Hello, ${name}!" }
println "Add: ${add(3, 5)}"
println "Multiply: ${multiply(4, 6)}"
println "Greet: ${greet('World')}"
// Closures as first-class objects
def operations = [add: add, multiply: multiply]
println "Operations: add(10,20) = ${operations.add(10, 20)}"
Output
Add: 8 Multiply: 24 Greet: Hello, World! Operations: add(10,20) = 30
What happened here: Closures stored in def variables can be called just like methods. You can even store them in maps and invoke them dynamically. This is a taste of functional programming in Groovy.
Example 11: Def with the Implicit “it” Parameter
What we’re doing: Using closures where the single parameter is implicitly named it.
Example 11: Implicit it
def numbers = [1, 2, 3, 4, 5]
// 'it' is the implicit parameter name
def doubled = numbers.collect { it * 2 }
def evens = numbers.findAll { it % 2 == 0 }
def sum = numbers.inject(0) { total, num -> total + num }
println "Doubled: ${doubled}"
println "Evens: ${evens}"
println "Sum: ${sum}"
Output
Doubled: [2, 4, 6, 8, 10] Evens: [2, 4] Sum: 15
What happened here: When a closure takes a single parameter, Groovy provides the implicit name it. You don’t need to declare it – just use it directly. This works because the closure parameter is dynamically typed (like def).
Example 12: Def vs var (Groovy 5.x)
What we’re doing: Comparing def with Java’s var keyword in Groovy 5.x.
Example 12: def vs var
// def - dynamically typed (Object at compile time)
def defVar = "Hello"
println "def type: ${defVar.getClass().simpleName}"
// var - type inferred at compile time (Groovy 3.0+)
var varName = "Hello"
println "var type: ${varName.getClass().simpleName}"
// Both produce the same output, but var provides better IDE support
// because the type is inferred at compile time
def list1 = [1, 2, 3] // Compiled as Object
var list2 = [1, 2, 3] // Compiled as ArrayList
println "def list: ${list1.getClass().simpleName}"
println "var list: ${list2.getClass().simpleName}"
Output
def type: String var type: String def list: ArrayList var list: ArrayList
What happened here: Since Groovy 3.0, you can use var as a type-inferred local variable declaration – similar to Java 10’s var. The runtime behavior looks identical, but var gives the compiler and IDE more type information, resulting in better code completion and earlier error detection.
Def with Closures and Methods
Method Parameters: Def vs Typed
Method Parameters
// Untyped parameters - maximum flexibility
def process(data) {
println "Processing: ${data} (${data.getClass().simpleName})"
}
process("text")
process(42)
process([1, 2, 3])
// Typed parameter - documents expected input
String formatName(String first, String last) {
"${last}, ${first}"
}
println formatName("John", "Doe")
Output
Processing: text (String) Processing: 42 (Integer) Processing: [1, 2, 3] (ArrayList) Doe, John
Untyped parameters accept anything – great for utility methods. Typed parameters self-document the API and catch misuse earlier.
Static Typing with @CompileStatic
If you want the performance and safety of static typing without giving up Groovy syntax, use @CompileStatic:
@CompileStatic
import groovy.transform.CompileStatic
@CompileStatic
class Calculator {
int add(int a, int b) {
return a + b
}
double divide(double a, double b) {
if (b == 0) throw new ArithmeticException("Division by zero")
return a / b
}
}
def calc = new Calculator()
println "Add: ${calc.add(10, 20)}"
println "Divide: ${calc.divide(10.0, 3.0)}"
Output
Add: 30 Divide: 3.3333333333333335
With @CompileStatic, Groovy checks types at compile time and generates bytecode similar to Java – faster execution but no dynamic features. Use it for performance-critical code. Learn more about Groovy’s type system in our Traits and File Operations guides.
Edge Cases and Best Practices
Edge Case: Def with Boolean Evaluation
Groovy Truth with Def
// Groovy truth - these are all "falsy"
def values = [null, 0, "", [], false, [:]]
values.each { val ->
println "${val?.toString()?.padRight(6) ?: 'null '} → ${val ? 'truthy' : 'falsy'}"
}
Output
null → falsy
0 → falsy
→ falsy
[] → falsy
false → falsy
[:] → falsy
Groovy has its own “truthiness” rules. Null, zero, empty strings, empty collections, and false are all falsy. This works well with def variables because the check happens at runtime.
Best Practices Summary
DO:
- Use
deffor local variables where the type is obvious from the value - Use explicit types for method parameters and return types in public APIs
- Use
@CompileStaticfor performance-critical code - Use
varwhen you want type inference with better IDE support
DON’T:
- Use
deffor GORM domain class properties (GORM needs explicit types) - Reassign variables to completely different types in production code
- Skip
defin scripts unless you intentionally want binding variables
Performance Considerations
Using def has a small runtime cost compared to explicit types. Every method call on a def variable goes through Groovy’s Meta-Object Protocol (MOP) for dynamic dispatch, while explicitly typed variables with @CompileStatic use direct JVM invocation.
For most applications, this difference is negligible. But for tight loops processing millions of records, consider using explicit types with @CompileStatic:
Performance Comparison
import groovy.transform.CompileStatic
// Dynamic (def) - slower for tight loops
long dynamicSum() {
def sum = 0
for (def i = 0; i < 1_000_000; i++) {
sum += i
}
return sum
}
// Static (@CompileStatic) - Java-speed
@CompileStatic
long staticSum() {
long sum = 0
for (int i = 0; i < 1_000_000; i++) {
sum += i
}
return sum
}
def start1 = System.nanoTime()
println "Dynamic: ${dynamicSum()}"
def time1 = (System.nanoTime() - start1) / 1_000_000
def start2 = System.nanoTime()
println "Static: ${staticSum()}"
def time2 = (System.nanoTime() - start2) / 1_000_000
println "Dynamic time: ${time1}ms"
println "Static time: ${time2}ms"
Output
Dynamic: 499999500000 Static: 499999500000 Dynamic time: ~45ms Static time: ~5ms
The static version is significantly faster for numerical computation. But for most code – reading files, handling web requests, processing data – the difference is irrelevant. Use def by default and optimize with @CompileStatic only where profiling shows a bottleneck.
Common Pitfalls
Pitfall 1: Def in Class Fields vs Script Variables
Problem: Confusing scope when using def in different contexts.
Wrong Way
// In a script, this creates a local variable
def message = "hello"
void printMessage() {
println message // MissingPropertyException!
}
Solution:
Correct Way
// Without def, it goes into the script binding
message = "hello"
void printMessage() {
println message // Works - accesses binding
}
printMessage()
Output
hello
Pitfall 2: Using Def with Return Statements
Problem: Forgetting that Groovy methods return the last expression.
Subtle Return Behavior
def getStatus() {
def status = "active"
println "Setting status..."
// Oops - println returns null, and that becomes the return value
}
def result = getStatus()
println "Result: ${result}" // null, not "active"!
Output
Setting status... Result: null
Fix: Either use an explicit return status or make sure the last expression is what you want to return.
Conclusion
The Groovy def keyword is one of the language’s signature features. It removes type ceremony while keeping your code flexible and readable. It works for declaring variables, writing methods, and storing closures – letting you focus on what your code does rather than what types it uses.
The bottom line is balance. Use def for convenience in local code, explicit types for API clarity, and @CompileStatic when performance matters. Groovy gives you the choice – that’s what “optionally typed” means.
Try these examples in your IDE or Groovy Console. Experiment with changing def to explicit types and see how the behavior shifts.
Summary
defdeclares a dynamically typed variable – it compiles toObject- Groovy is optionally typed – you can use
defor explicit types interchangeably - In scripts,
defcreates local variables; withoutdef, variables go into the binding - Use
@CompileStaticfor Java-speed static typing within Groovy classes - Since Groovy 3.0,
varprovides type inference with better IDE support thandef
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 -e Command Line Option – Run Scripts Instantly
Frequently Asked Questions
What does def mean in Groovy?
The def keyword in Groovy declares a variable, method, or property without specifying its type. It’s shorthand for Object – the variable’s actual type is determined at runtime based on the assigned value. It enables Groovy’s dynamic typing feature.
Is def the same as Object in Groovy?
Yes, under the hood, def compiles to java.lang.Object in the bytecode. The difference is syntactic – def is shorter and signals to other developers that you’re intentionally using dynamic typing. Both def and Object allow the variable to hold any type.
Should I use def or explicit types in Groovy?
Use def for local variables where the type is obvious from context. Use explicit types for public API method parameters and return types, GORM domain properties, and performance-critical code with @CompileStatic. Groovy is optionally typed, so you can mix both freely.
What is the difference between def and var in Groovy?
def makes a variable dynamically typed (compiled as Object). var (available since Groovy 3.0) uses type inference – the compiler determines the type from the assigned value. var gives better IDE code completion and catches type errors at compile time, while def defers everything to runtime.
Does using def make Groovy slower than Java?
For most applications, no noticeable difference. But in tight computational loops, def variables use dynamic dispatch which is slower than direct JVM method invocation. For performance-critical sections, use explicit types with @CompileStatic to get Java-equivalent speed.
Related Posts
Previous in Series: Setting Up Groovy IDE – IntelliJ IDEA, VS Code, and Eclipse
Next in Series: Groovy -e Command Line Option – Run Scripts Instantly
Related Topics You Might Like:
- Groovy Closures – The Complete Guide
- Groovy Traits – Reusable Behavior Composition
- Groovy String Tutorial – The Complete Guide
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment