12 Groovy Def Keyword Examples – Dynamic Typing Explained

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.

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:

  • def makes a variable dynamically typed – the type is resolved at runtime
  • Under the hood, def compiles to Object
  • A variable declared with def can 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 def entirely – 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:

ScenarioUseWhy
Local variables with obvious valuesdefType is clear from the right side
Method parameters in APIsExplicit typeDocuments what callers should pass
Method return types in APIsExplicit typeDocuments what callers get back
Scripts and quick prototypesdefSpeed over formality
Performance-critical codeExplicit type + @CompileStaticEnables compile-time optimization
Domain class propertiesExplicit typeGORM 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:

  1. Compilation: def variables compile to type Object in the bytecode
  2. Assignment: The actual value gets stored with its real type (Integer, String, etc.)
  3. Method calls: Groovy’s runtime looks up the actual type and dispatches the right method
  4. 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 name or String 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 def for local variables where the type is obvious from the value
  • Use explicit types for method parameters and return types in public APIs
  • Use @CompileStatic for performance-critical code
  • Use var when you want type inference with better IDE support

DON’T:

  • Use def for GORM domain class properties (GORM needs explicit types)
  • Reassign variables to completely different types in production code
  • Skip def in 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

  • def declares a dynamically typed variable – it compiles to Object
  • Groovy is optionally typed – you can use def or explicit types interchangeably
  • In scripts, def creates local variables; without def, variables go into the binding
  • Use @CompileStatic for Java-speed static typing within Groovy classes
  • Since Groovy 3.0, var provides type inference with better IDE support than def

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.

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:

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 *