@Incubating
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.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:
owner, delegate, thisObject, resolveStrategy,
directive, metaClass or super — it needs a real Closure;Serializable type, passed
directly to a writeObject call, or held in a local that is) — it keeps its class
so serialization works;this(...)/super(...) constructor call, inside a trait, or cast to an
intersection type;this-property — that the
delegate chain or the MOP could intercept;@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.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):
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;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;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.| Type | Name and Description |
|---|---|
PackedClosures.PackMode |
modeThe packing behaviour of this scope: PackMode.LENIENT (pack, silent on declines, the default), PackMode.WARN (pack, a compiler warning per decline with the reason), PackMode.STRICT (pack, a compiler error on any decline), or PackMode.DISABLED (do not pack). |
The packing behaviour of this scope: PackMode.LENIENT (pack, silent on declines, the
default), PackMode.WARN (pack, a compiler warning per decline with the reason),
PackMode.STRICT (pack, a compiler error on any decline), or PackMode.DISABLED
(do not pack). The most-specific declaration wins, so a DISABLED method opts out of a
packed class (and DISABLED also overrides the automatic groovy.target.closure.pack
flag); the WARN/STRICT diagnostics apply only to the annotated scope, never to the flag.