Skip to content
Lesson 7 of 7

Step 1 of 4 · Reading · ~3 min

Learn

Spock, Gradle, Modern Groovy

Groovy is dynamic by default. Every method call goes through reflection-like dispatch. Modern features let you opt into static dispatch for performance.

Default: dynamic

def twice(x) {
    x * 2
}

twice(5)         // 10
twice("hi")      // "hihi" — String has its own multiply
// `double` would not compile as a method name here: it is a Java
// primitive type name, and Groovy inherits that reservation.

At each call the runtime looks up a multiply method on whatever x turned out to be — Integer.multiply one time, String.multiply the next. That lookup is the cost of the flexibility.

@CompileStatic

import groovy.transform.CompileStatic

@CompileStatic
def sum(List<Integer> nums) {
    int total = 0
    for (int n : nums) {
        total += n
    }
    return total
}
  • Method calls resolved at compile time
  • Type errors caught early
  • Closer to Java performance
  • metaClass mods don't apply

Benchmarks: dynamic Groovy is often 2-5x slower than static Groovy. For hot loops, the difference is measurable.

When to use which

Static (@CompileStatic):

  • Performance-critical code
  • Library code where types matter
  • Refactoring safety (compile errors catch breakage)

Dynamic (default):

  • DSLs (Gradle, Spock — they NEED dynamic)
  • Configuration code
  • Glue scripts
  • Where flexibility outweighs speed

@TypeChecked

Checks types at compile time but keeps dynamic dispatch:

import groovy.transform.TypeChecked

@TypeChecked
def greet(String name) {
    return "Hello, $name"
}

// greet(42)  // compile error: Integer != String

Compromise — type safety without losing metaprogramming.

Static type checking caveats

@CompileStatic
def ambiguous(map) {
    map.size()        // ERROR — what is map's type?
}

Without a type annotation the parameter is Object, and Object has no size(), so the call cannot be resolved. Writing def map does not help — def on a parameter means Object. Give it a real type:

  • Explicit types: Map map, List<Integer> nums
  • Generic params: def <T> processList(List<T> items)

Performance comparison

Dynamic Groovy:    100 ns/op
@TypeChecked:        80 ns/op  (slight win — early type checks)
@CompileStatic:      30 ns/op  (3x faster)
Pure Java:          25 ns/op  (5% faster than @CompileStatic)

Numbers vary; measure your code.

Mixing static and dynamic

You can apply @CompileStatic per-class, per-method, or globally:

// Class-level — applies to all methods
@CompileStatic
class Calculator { ... }

// Method-level
class Calculator {
    @CompileStatic
    int fastAdd(int a, int b) { a + b }

    def slowAdd(a, b) { a + b }    // dynamic
}

// File-level (with @CompileStatic on a top-level class)

Indy (invokedynamic) backend

Groovy 2.1+ has an invokedynamic mode that dramatically speeds up dynamic calls (60-80% of static performance). Enable with the -indy JAR. Most modern Groovy uses indy by default.

When the dynamism matters

  • Spock: relies on methodMissing, AST transformations
  • Gradle: every block's delegate magic
  • HTTP/JSON builders: json.user { ... }user is intercepted via methodMissing
  • Mocking libraries: Mock(Class) generates dynamic stubs

These DON'T work with @CompileStatic. Keep that code dynamic; static-compile your business logic.

The verdict

  • New Groovy code: prefer @CompileStatic unless you NEED dynamic
  • DSLs and config: keep dynamic
  • Performance: profile first; static usually wins
  • Maintenance: static catches more bugs at compile time

Groovy started as fully dynamic — its evolution toward optional static is a recognition that dynamic wasn't always the right answer.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…