Annotation Interface PackedClosures


@Incubating @Retention(SOURCE) @Target({TYPE,METHOD}) public @interface PackedClosures
Opt-in for compact closure compilation: within the annotated class or method, an eligible closure literal's body is hoisted into a synthetic method on the enclosing class and the literal is replaced by a shared PackedClosure adapter, instead of generating one inner class per closure. The value stays a real groovy.lang.Closure, so curry, memoize, trampoline and iteration keep working; captured values are threaded by value (read-only) or via a shared groovy.lang.Reference (when written), so a packed closure behaves identically to the class-based form.

Packing is best-effort: a closure that cannot be packed is compiled exactly as today (a generated closure class), so the annotation is always safe to add. A closure is kept as a class when it:

  • references owner, delegate, thisObject, resolveStrategy, directive, metaClass or super — it needs a real Closure;
  • has default parameter values, or contains an anonymous inner class;
  • is nested inside another closure, or visibly escapes its method (returned, stored to a field/property/index, initialising a field, appended, or placed in a collection literal);
  • is visibly serialization-bound (cast or coerced to a Serializable type, passed directly to a writeObject call, or held in a local that is) — it keeps its class so serialization works;
  • is written where the adapter cannot stand in for a generated class: as an argument to a this(...)/super(...) constructor call, inside a trait, or cast to an intersection type;
  • under dynamic compilation, resolves any free name — an implicit-this call, a bare field/property-bound or dynamic variable, or an explicit this-property — that the delegate chain or the MOP could intercept;
  • under @CompileStatic, resolves a name against a delegate (e.g. via @DelegatesTo or with), is left to runtime resolution by a type-checking extension (mixed-mode dynamic islands), or touches this-properties of a Map-implementing owner — packing is otherwise proven sound from the type checker's resolution.
Under dynamic compilation the annotation is a trust assertion the compiler cannot verify; the PackedClosure adapter then fails fast if a delegate, or a delegate-consulting resolveStrategy, is later set on a packed closure. Automatic packing of provably-safe @CompileStatic closures — without this annotation — is available behind the groovy.target.closure.pack flag. Use mode() to have declines reported (or to opt a scope out); on the un-annotated flag path, groovy.target.closure.pack.report=true reports every decline with its reason as a compiler warning — the operational way to see where the packability boundary falls in a codebase.

Because every packed closure is an instance of a small fixed-arity adapter family (PackedClosure$Fixed0..4, $FixedIt, $FixedN) rather than its own class, three differences from the class-based form remain that cannot, in general, be detected at compile time (the visibly serialization-bound cases above are the detectable exception):

  • Serialization: a packed closure is not serializable — attempting it fails fast at runtime with a message naming the closure and this opt-out (note dehydrate() cannot make it serializable — the dispatch state remains — although a dehydrated packed closure stays callable), so scopes that serialize their closures should not be packed;
  • Class identity: closure.getClass() distinguishes arity (enough for class-level introspection such as SAM-overload selection) but not individual literals, so code keyed on per-closure generated class names or types will not find them;
  • Class-level metaclass changes: modifying a family member's metaclass affects every packed closure of that arity, where a per-closure-class change was scoped to one literal. Per-instance setMetaClass is fully honoured — a packed closure whose metaclass has been replaced or wrapped routes all dispatch through it, exactly as a generated closure class does.
See Also: