GEP-22
Abstract: Traits
Traits are a structural construct of the Groovy language that allow:
-
composition of behaviour
-
runtime implementation of interfaces
-
behaviour overriding
-
compatibility with static type checking and static compilation
Conceptually a trait is an interface that may carry both default implementations
and state. A class declares its participation in a trait via the implements
clause exactly as it would for any other interface. Traits compose into the
implementing class at compile time (or, optionally, at runtime via coercion)
without requiring multiple inheritance of classes.
This GEP specifies the language semantics of traits, captures the bytecode
compilation model, and records the evolution of the feature across Groovy
releases. Worked examples and tutorial-style code samples live in the
language specification (_traits.adoc); this document is intentionally
terse and prescriptive.
Motivation
Object-oriented codebases routinely need to reuse behaviour across class hierarchies that cannot share a common ancestor. The available pre-existing mechanisms each carry trade-offs:
-
Single inheritance forces all reuse through a single line, leading to deep hierarchies, fragile base-class problems, and inability to mix unrelated capabilities.
-
Java 8 default methods attach behaviour to interfaces but are stateless by design, do not support stackable composition through
super, and their resolution rules are designed around binary compatibility rather than composition. -
Runtime mixins (Groovy’s pre-2.3
@Mixinannotation, since deprecated) weave behaviour at runtime but the resulting object is not aninstanceofthe mixed-in type, defeating type-based dispatch and compile-time checks. -
Delegation via
@Delegateexpresses has-a relationships well but is verbose for genuine is-a composition and does not support overriding delegated behaviour transparently.
Traits address these gaps. Drawing on the work of Schärli, Ducasse, Black and Nierstrasz (ECOOP 2003), and on Scala’s analogous construct, a trait combines the contract role of an interface with the reusable-implementation role of a class while imposing deterministic composition rules. As Cédric Champeau put it when introducing them: traits extend the benefit of interfaces to concrete classes without producing inheritance pyramids, allowing new APIs to be layered onto existing classes without modification.
A side-by-side comparison with Scala traits, Kotlin interfaces, and Java default methods is given in Comparison with related constructs below.
Specification
Declaration
A trait is declared with the trait keyword:
trait FlyingAbility {
String fly() { "I'm flying!" }
}
The annotation @groovy.transform.Trait is an exact synonym and may be
applied to any class declaration that would otherwise satisfy the trait
shape; the AST transform produces equivalent output. The trait keyword
is preferred in source.
A trait may declare any of: methods (public or private, abstract or
concrete, static or instance), properties, fields (public or private,
instance or static), and an implements clause naming interfaces and/or
super-traits. A trait may extend at most one super-trait via extends,
or any number of super-traits by listing them in implements.
Constructors are not permitted on traits.
Methods
-
Public and private methods are supported.
protectedand package-private are not supported. -
Abstract methods are permitted; concrete classes that implement the trait must provide an implementation unless they too are abstract.
-
Private methods do not appear on the generated trait interface and are not visible to other classes.
-
The
finalmodifier records the intended modifier of the woven method in the implementing class. Mixingfinaland non-finaldeclarations of the same signature across multiple inherited traits is permitted; normal trait method-selection rules apply and the resulting method takes the modifier of the selected source.
Fields
Fields declared in a trait are not stored on the generated trait interface (interfaces cannot hold instance state). Instead they are stored on a synthetic field helper class and woven into the implementing class under name-mangled identifiers:
-
For a field
barof typeTdeclared in traitmy.pkg.Foo, the implementing class gains a field namedmy_pkg_Foo__barof typeT. -
The mangling rule is: replace each
.in the package name with_, append<traitSimpleName>__<fieldName>. The double underscore separates the trait identity from the field identity. -
Private fields follow the same scheme but are also marked private; the mangling guarantees no collisions when a class implements multiple traits that declare fields of the same name (the diamond problem for state).
Use of public fields in traits is discouraged in favour of properties.
Properties
A property declared in a trait yields the usual auto-generated accessor
pair (getX/setX, or isX for boolean) on the generated trait
interface, and a backing field on the implementing class woven via the
field-helper mechanism. Property semantics are otherwise identical to
those of a regular Groovy class.
this, super, and stackable traits
Inside a trait method:
-
thisrefers to the implementing instance, never to a trait-level artefact. A trait should be reasoned about as if it were a superclass of the implementing class, despite the fact that no such class exists in the runtime hierarchy. -
An unqualified
super.m(…)call delegates to the next trait in the implementation chain (in the order produced by theimplementsclause of the implementing class, walked right-to-left). When the chain is exhausted,superresolves to the actual superclass of the implementing class. (This applies to instance methods only; from astatictrait method it is rejected — see the unsupported-syntax list below.) -
A qualified
T.super.m(…)call resolves to traitT’s implementation of `m, regardless of position in the chain. This is the explicit override form.
This rule set yields stackable traits: behaviours that delegate to the
next trait via unqualified super can be composed in different orders
to produce different overall behaviours, without those traits needing to
know about each other.
The following qualifier syntax is not supported inside trait code and is rejected at compile time:
-
T.this.*(GROOVY-12104).T.thisis Java’s enclosing-instance access for inner classes, and a trait is not an enclosing scope of its implementer (per item 1,thisis the implementing instance directly), so it has no coherent meaning here. The rejection covers both method calls (T.this.m(…)) and field/property access (T.this.x). For normal dispatch usethis.m(…)(item 1); for explicit trait-anchored dispatch useT.super.m(…)(item 3). -
unqualified
super.m(…)from astatictrait method (GROOVY-12105). The unqualifiedsuperform of item 2 is supported for instance methods only; from astatictrait method the trait chain is not walked. UseT.super.m(…)for the trait’s own staticm, or call the implementer-side method by name without a qualifier. (This restriction is specific to traits: Groovy permits unqualifiedsuper.m(…)instaticmethods of plain classes, where it dispatches to the actual superclass’s static method.)
Multiple inheritance and conflict resolution
When a class implements two or more traits that supply conflicting
implementations of a method with the same signature, the last trait
listed in the implements clause wins. This is the deterministic
default. The implementing class may override the resolution:
-
by providing its own implementation of the method, or
-
by calling
T.super.m(…)for the desired traitT.
The same rule applies to property accessors and field-helper-backed state. Diamond conflicts on field state cannot arise: each trait’s fields live under their own mangled names.
SAM coercion
A trait that declares exactly one abstract method is a Single Abstract
Method type for the purposes of closure coercion. A Closure may be
assigned or coerced to such a trait, in which case the closure body
becomes the implementation of the abstract method. SAM coercion of
traits composes with the rules in GEP-12.
Runtime application
Traits may be applied to an existing object at runtime:
def proxy = subject as SomeTrait
def proxy = subject.withTraits(TraitA, TraitB)
Both forms produce a new proxy instance — never the original object — that:
-
implements
SomeTrait(orTraitAandTraitB), -
implements every interface that the original object implemented, and
-
delegates to the original object for behaviour not supplied by the applied trait(s).
A trait method always takes precedence over the corresponding method on
the proxied object when both exist. The proxy is not an instance of
the original class; consumers that rely on instanceof against the
original concrete class must continue to use the underlying object.
Static members (Incubating)
Static methods, properties and fields in a trait are supported.
The behaviour described below reflects the
Groovy 6.0 model — declarer-bound dispatch for public trait statics,
with promotion onto the generated trait interface as JVM-native
interface statics, and an opt-in @groovy.transform.Virtual marker
that restores per-implementer override dispatch for callees that need
it (e.g. Grails' Validateable.defaultNullable() and similar
framework hooks).
Trait rules apply in three distinct phases; it helps to keep them apart when reading the rules that follow:
-
Defining a trait — checks that run when the trait itself is compiled, independent of
@CompileStatic. Malformed trait code is rejected outright (for example a misapplied@Virtual, aT.this.*qualifier, or unqualifiedsuperfrom a static trait method — see this, super, and stackable traits). These rejections always apply, in dynamic and statically-checked code alike, because you cannot even define such a trait. -
Weaving a trait — what happens when a class names the trait in its
implementsclause: per-direct-implementer composition, the bridge forwarders, and the static-helper plumbing (see Bytecode and stub model). -
Using a trait — resolving a call at its call site, which follows the compilation mode. Under
@CompileStatic/@TypeCheckedan unresolvable call is a compile-time error; in dynamic code the same call instead fails at runtime withMissingMethodException. So the resolvability rules below describe the same outcome reported at two different times depending on how the calling code is compiled. -
Each implementing class receives its own copy of static state. A static field declared in a trait is conceptually a template, instantiated per implementer; static state is not shared across implementing classes.
-
Public static methods declared in a trait are promoted onto the generated trait interface as JVM-native interface statics (GROOVY-12111). External calls of both forms —
Trait.staticMethod(…)against the trait itself andImpl.staticMethod(…)against an implementing class — are supported. -
Static trait methods, fields and properties may be declared in, and called from,
@CompileStaticand@TypeCheckedcontexts. Calls inside a statically-checked trait body are subject to the ordinary type-checking and resolvability rules. -
A public static method declared in a trait is declarer-bound by default: a call of the form
m(…)orthis.m(…)made directly in the body of a trait method resolves to the trait’s own copy regardless of any same-signature static method declared on the implementing class. The implementer’s same-name static is an independent method, not an override; externalImpl.m()reaches that method, but trait-body calls do not see it. -
A public static method may be annotated
@Virtualto opt into per-implementer override dispatch. Trait-body calls of the formm(…)orthis.m(…)to a@Virtualcallee are routed through the implementing class at runtime, so a same-signature static declared on the implementer shadows the trait’s default from inside trait code. A@Virtualmethod is not promoted onto the trait interface (interface statics are declarer-bound by JVM rule, which is incompatible with virtual dispatch). It therefore has no qualifiedTrait.m(…)form: a bareTrait.m(…)names the trait with no implementing class to dispatch through, so it is rejected — a compile-time error under@CompileStatic/@TypeChecked(GROOVY-12112), and a runtimeMissingMethodExceptionin dynamic code. Read a concrete class’s value withImpl.m(…), or useT.super.m(…)from within trait code for the trait’s own definition. -
@Virtualis valid only on public static (non-abstract) trait methods. Applying it elsewhere — to an instance method, a private method or an abstract method — is a compile-time error. -
A private static method declared in a trait is trait-internal: it is not composed onto the implementing class, is not overridable, and is not promoted onto the trait interface.
-
A static method call made from inside a closure body inside a trait method dispatches through the trait helper rather than through the implementing class. An implementer’s override is not visible from such a call site, regardless of whether the callee is marked
@Virtual. -
A reference to a static field from trait code reads the trait’s template field, not any same-named static on the implementing class. Per-implementer overridable defaults should be expressed through a
@Virtualaccessor method, not a field (cf. the inheritance-of-state gotcha in Limitations). For example:import groovy.transform.Virtual trait V { @Virtual static String getOrigin() { 'trait' } // overridable default static String describe() { "origin is ${origin}" } } class Over implements V { static String getOrigin() { 'class' } // visible to trait body } assert Over.describe() == 'origin is class'Inside the body of
describe(), the property referenceoriginresolves through Groovy’s getter chain togetOrigin(), which is@Virtualand so dispatches through the implementing class, picking up the override. -
Trait composition occurs at the class that names the trait in its
implementsclause. A subclass of an implementer does not re-compose the trait; same-name static members declared on the subclass are not visible to trait code, because the trait’s forwarder dispatches through the direct implementer. -
A
T.m()call made from within traitT's own body resolves through the trait interface’s JVM-native static (the promotion described above), and so reaches the trait’s own copy — the same trait-anchored dispatch that anyTrait.m()caller sees from outside the trait. This applies to plain (non-@Virtual) statics only; a@Virtualcallee has no qualified form, soT.m()from trait code is rejected the same way as any otherTrait.m(…)call (GROOVY-12112) — use unqualifiedm(…)/this.m(…)for override-visible dispatch, orT.super.m(…)for the trait’s own default. -
Static methods declared in a super-trait are inherited by a sub-trait and may be called from the sub-trait’s own body, resolved by the same rules as a static declared in the trait itself: an unqualified
m(…)orthis.m(…)call is declarer-bound (or, for a@Virtualcallee, dispatches through the implementing class so an implementer’s override is visible); aParent.m(…)call resolves through the super-trait’s promoted interface static (for the non-@Virtualstatics that are promoted there); andParent.super.m(…)reaches the super-trait’s own copy. Argument resolution is subtype-aware — an argument whose static type is a subtype of the declared parameter type resolves just as it does for plain-class static inheritance (GROOVY-12106). For example:@CompileStatic trait ExecutesClosures { static String withDelegate(Closure callable, Object delegate) { /* ... */ "for ${delegate.class.simpleName}" } } final class Argument { } @CompileStatic trait Arguable extends ExecutesClosures { String handle(Argument arg) { withDelegate({ -> }, arg) } // inherited static; arg is a subtype of Object } class C implements Arguable { } assert new C().handle(new Argument()) == 'for Argument' -
Mixing static and instance methods of the same signature across multiple traits is undefined behaviour and should be avoided. If selection chooses a static variant where an instance variant is required, a compilation error is raised; conversely, an instance selection silently shadows the static variant.
-
A trait method (static or instance) that collides with a same-signature method inherited by the implementing class from its superclass shadows the inherited one at any call site where the trait method is reachable. At an instance call site (
new D().m()), the trait member takes precedence regardless of whether either member is static or instance; at a static call site (D.m()), the trait member wins when it is itself static, while the inherited class static is reached only when the trait member is instance (and therefore unreachable via a static call). -
To disambiguate such a collision, declare
mon the implementing class and from its body invokesuper.m()for the superclass version andT.super.m()for the trait version. Both super-call forms work uniformly for static and instance trait members;T.super.m()is the canonical escape for any trait-vs-superclass collision.
The principal dispatch behaviour summarised:
| Trait member declaration | Trait-body access | External access | Override visible? | Disambiguation / escape |
|---|---|---|---|---|
|
Dispatches to the trait’s own copy (declarer-bound) |
Both |
No |
Mark |
|
Dispatches through the implementing class |
|
Yes |
|
|
Dispatches to the trait’s own copy (trait-internal) |
Not visible |
No (not overridable) |
— |
|
Reads the trait’s template field |
|
No |
Use a static accessor pair instead — see the worked example above |
|
Property syntax |
|
Yes (the |
(the supported pattern for overridable static defaults) |
Public instance method |
Dispatches polymorphically |
|
Yes |
|
When to use @Virtual
A plain trait static is a fixed part of the trait: trait code that
calls it always reaches the trait’s own copy, and an implementing class
cannot change what the trait sees. That is the right default for an
internal helper or a constant — something the trait owns outright.
@Virtual is for the opposite case: a static method that the trait
wants implementing classes to be able to override, and whose override
the trait’s own logic must honour. It is the template-method pattern —
a default plus a per-class override — applied to a static member,
mirroring the override-visibility that trait instance methods already
have.
Reach for it when the overridable thing is genuinely a property of the type rather than of an instance — a policy or configuration default that:
-
answers a question about the class as a whole ("are properties nullable by default for this entity?", "what is the default page size?");
-
may be consulted where no instance exists yet — during class initialisation, in static factory methods, or while building metadata;
-
external callers should be able to read statically as
Impl.setting().
This is the pattern Grails uses for hooks such as
Validateable.defaultNullable(): the framework ships a default, a domain
class may override it with its own static, and the framework’s own
(static) logic must see the per-class value. For overridable static
data rather than behaviour, expose it through a @Virtual accessor
(@Virtual static getSetting()) rather than a static field, since a
bare static-field read from trait code is not override-visible (see the
worked example above).
If the member is instead an internal helper, a constant, or anything the
trait should keep authority over, leave it a plain static:
declarer-bound is the safer default, and an implementer’s same-named
static then stays an independent method rather than silently changing the
trait’s behaviour.
|
Note
|
Why the obvious alternatives do not fit a class-level overridable default:
|
The call forms follow the use case: the trait consults the setting with
setting() or this.setting(); an implementing class overrides it with a
plain static setting(); external code reads a concrete class’s value
with Impl.setting(); and T.super.setting() reaches the trait’s own
default explicitly. A bare Trait.setting() is not part of the pattern
— it has no implementing class to dispatch through and is rejected
(GROOVY-12112; see the @Virtual rule above).
@SelfType
@groovy.transform.SelfType declares a list of types that any class
implementing the annotated trait must extend or implement. The compiler
verifies the constraint at compile time and reports a clear error if it
is violated. Self types make traits that depend on the surrounding
class’s API safe to type-check and statically compile, without resorting
to explicit (this as SomeBaseClass) casts inside trait method bodies.
Interaction with @Sealed
Traits may be sealed (see GEP-13). @Sealed and @SelfType are
orthogonal:
-
@SelfTypeconstrains what an implementing class must already be (its supertype shape). -
@Sealedconstrains which specific classes are allowed to implement the trait at all.
A trait may carry both annotations. For the degenerate single-permitted-
implementer case, @SelfType(Foo) is preferred over
@Sealed(permittedSubclasses = ['Foo']) for clarity.
Generics
Generics in a trait behave the same as generics in an ordinary class or
interface. The implementing class sees the trait’s type parameters exactly
as if it extended an abstract generic superclass that bound them — the
superclass model, not the bare generic-interface model, because a trait
contributes state and method bodies, not only signatures. Bounded and
recursive (F-bounded) type parameters, wildcards, type-parameter narrowing
in a sub-trait (trait Foo extends Bar<String>), bounded method-level type
parameters, and generic return types all follow the rules they would
outside a trait.
The …but is that traits have no native generics: the four-class model (Bytecode and stub model) reconstructs ordinary generic semantics across the trait interface, the static helper, the field helper, and the woven bridges. That reconstruction is faithful, but it happens at seams that have no analogue outside traits, and the following points are therefore specified rather than left implicit:
-
A trait’s class-level type parameter is promoted to a method-level type parameter on each generated static helper method (a
staticmethod cannot reference an enclosing type variable). Bounds are carried across the promotion intact, including intersection bounds (T extends A & B) and F-bounds (T extends Comparable<T>). -
A method-level type parameter that shadows a class-level one of the same name is renamed when both are flattened onto a single helper static, preserving the shadowing semantics without a JVM-level name clash.
-
The generic signature on the trait interface (against which a call from an implementing class is type-checked) and the generic signature on the helper (through which the call dispatches, and against which a trait-body call is checked) agree, so a call resolves identically from both sites.
-
A generic field — including a parameterised collection field — survives field-helper remapping with its fixed type argument, so
@CompileStaticimplementer code sees the reified element type. -
A
staticmethod inherited from a generic super-trait resolves from a sub-trait body with subtype-aware argument matching, exactly as for plain-class static inheritance (GROOVY-12106; see Static members). -
Self-types (
trait T<SELF extends T<SELF>>) and stackableT.super.m(…)calls preserve the type parameter through the lowering.
Error behaviour matches non-trait generics: a class that reaches the same generic super-trait through two incompatible bindings is rejected at compile time, as it would be for a generic interface. (The diagnostic currently reports the resulting accessor-return-type incompatibility rather than naming the binding conflict directly; the outcome — rejection — is the specified behaviour.)
These behaviours are pinned by the TraitGenericsMatrix conformance suite
(package org.codehaus.groovy.transform.traitx).
Metaprogramming
Trait-contributed members participate in the runtime meta-object protocol
(MOP) the same as members declared on the implementing class itself: they
are visible to dynamic dispatch, to respondsTo/hasProperty
introspection, and to metaClass manipulation. For MOP purposes a trait is
indistinguishable from a superclass that declared the same members.
The …but is a short list of clarifications, plus the runtime-application capabilities that traits add and ordinary classes do not:
-
A trait may implement the MOP hook methods
methodMissing,propertyMissingandinvokeMethod; they are woven onto the implementing class and handle that class’s otherwise-unresolved calls and property accesses. -
A real (woven) trait method always takes precedence over the implementer’s
methodMissing/propertyMissing: only genuinely unresolved names reach the hook. -
A trait’s
invokeMethod, in the absence ofGroovyInterceptable, acts as a missing-method fallback — it intercepts only calls that do not resolve to a real method — exactly as it would on a plain class. It is not a blanket interceptor unless the implementing class also implementsGroovyInterceptable. -
A trait method is an ordinary instance method on the implementer, so a runtime
metaClass/ExpandoMetaClassoverride of that method shadows the trait’s version, just as it would shadow a class-declared method. Trait methods carry no privilege against the MOP. -
Remapped public trait fields are reachable dynamically under their mangled names (Fields), through both property and subscript access.
The two trait-specific runtime capabilities — coercing an existing instance
to a trait with as, and applying traits to an instance with withTraits
— are specified in Runtime application; SAM
coercion of a single-abstract-method trait is specified in
SAM coercion. All three are metaprogramming entry points
with no equivalent on a plain class.
These behaviours are pinned by the TraitDynamicBehaviorMatrix conformance
suite (package org.codehaus.groovy.transform.traitx).
Bytecode and stub model
A trait pkg.T produces, after compilation, the following artefacts:
-
An interface
pkg.Tcontaining the abstract and public-method signatures, plus accessor signatures for any properties. This is the type that participates ininstanceof, generics, and Java interop. Public (non-@Virtual) static trait methods are also emitted here as JVM-nativestaticinterface methods that forward to the helper (GROOVY-12111); this is what makes both externalTrait.m()and from-traitT.m()resolve at the JVM level. The interface contains no default (instance) methods — trait instance-method bodies are not bytecode-default-method bodies, even on JDK 8+. -
A trait helper class named
pkg.T$Trait$Helper(constantTraits.TRAIT_HELPER). Each non-abstract trait methodm(args)is emitted as astaticmethod on the helper, taking the receiver as an explicit first parameter (and threading any captured trait state through field-helper accessors). Bridge methods are woven into each implementing class to forward instance calls to the helper. -
A field helper class named
pkg.T$Trait$FieldHelper(constantTraits.FIELD_HELPER). It exposes get/set accessors for trait instance fields, parameterised by the receiver. The implementing class is given mangled fields and small bridge accessors that delegate to this helper. -
A static field helper class named
pkg.T$Trait$StaticFieldHelper(constantTraits.STATIC_FIELD_HELPER) when the trait declares static fields, providing per-implementer storage for static state.
This four-class model is what makes traits visible to Java callers as
plain interfaces (with no default methods to surprise Java consumers),
while still supplying state, stackable super, and conflict resolution
that a Java-default-method approach cannot.
Limitations
-
AST transform compatibility is best-effort. Some transforms (
@CompileStatic,@TypeChecked, logging transforms) apply cleanly to traits; others apply only to the implementing class; others are unsupported. New transforms should be evaluated case by case. -
Prefix and postfix operators (
+`, `--`) on a trait field are rejected at compile time. The workaround is `=/-=. The reason is that the field-helper indirection cannot represent the read-modify-write sequence atomically. -
Constructors on traits are not supported. Initialisation logic should be expressed via abstract methods supplied by the implementing class, or via property defaults.
-
Inheritance-of-state gotcha: trait method bodies that read a trait field directly read the trait’s own field, not any same-named property declared by the implementing class. To pick up an overriding value, trait code should access state through accessors (
getX()), not by direct field reference. The same rule applies to static-field references — see Static members.
Comparison with related constructs
Groovy traits occupy a design space shared with Scala traits, Kotlin interfaces (including their companion-object machinery), and Java default methods. Each construct trades expressiveness for simplicity in its own way. The table below cross-references the load-bearing features of Groovy traits against the closest analogue in each language.
| Feature | Groovy trait |
Scala trait |
Kotlin interface |
Java interface (default methods) |
|---|---|---|---|---|
Instance state (fields) |
Yes. Woven into the implementing class via a field-helper class with name-mangled identifiers; no diamond-of-state risk. |
Yes. First-class |
No backing fields. Only abstract or computed properties are permitted. |
No state of any kind. |
Static methods |
Yes (incubating). Each implementing class receives its own copy. Public statics are declarer-bound by default and promoted onto the generated trait interface as JVM-native interface statics; |
Indirect, via a companion |
Indirect, via a |
Yes (since Java 8). Resolved against the declaring interface; not inherited by sub-interfaces or implementing classes. |
Static state (fields) |
Yes (incubating). Stored in a per-implementer static-field-helper class. |
Via the companion |
Via the companion |
Yes — one copy per interface. |
Constructors / parameters |
Not permitted. |
Scala 3 supports trait parameters (e.g. |
Not permitted. |
Not permitted. |
Method visibility |
|
Full range, including |
|
|
Inherited-method conflict resolution |
Last-wins by |
C3 linearization — deterministic right-to-left walk of the inheritance graph; |
Diamond is a compile error. The implementing class must resolve it explicitly via |
Diamond is a compile error. The implementing class must resolve it explicitly via |
Stackable |
Yes. Unqualified |
Yes. |
No. |
No. |
Self-types |
|
First-class syntax |
Not supported; expressed via bounded generics where unavoidable. |
Not supported. |
Runtime application to existing instances |
|
Only at construction: |
Not supported. |
Not supported. |
SAM coercion from a closure / lambda |
Yes, for a single-abstract-method trait. See GEP-12. |
Yes, via SAM types and function literals. |
Yes, for |
Yes, for any functional interface. |
Java-callable view of the construct |
A plain interface — no default methods. State and behaviour live in helper classes. |
A JVM interface containing |
A JVM interface; default-method generation is controlled by |
A JVM interface natively. |
A few qualitative observations follow from the table:
-
State is the dividing line. Only Groovy and Scala give traits true instance state; Kotlin and Java explicitly avoid it. Groovy’s field-helper indirection is the price paid for keeping the Java-visible view of a trait as a plain interface (no default methods, no surprise multiple-inheritance behaviour for Java consumers).
-
Conflict-resolution philosophies differ. Groovy chooses a deterministic default (last-wins) so that conflicts compile without intervention. Scala chooses a deterministic algorithm (linearization) that the developer is expected to internalise. Kotlin and Java refuse to choose and require the developer to resolve every conflict explicitly. Each position is internally consistent; Groovy’s optimises for ergonomics, Scala’s for expressiveness, and Java/Kotlin’s for safety against accidental composition.
-
Stackable
superis a Groovy/Scala feature. It is what enables the mixin-style decorator composition that motivates traits in the first place. Kotlin and Java interfaces cannot express the same pattern without auxiliary delegation. -
Static members are the rough edge in every language. Java and Kotlin treat them as per-interface (no inheritance, no overriding). Scala routes them through a separate companion object whose members live on a singleton rather than per implementer. Groovy is the only one of the four that gives each implementing class its own copy of static members, which is what enables patterns such as overriding static defaults from the implementing class (e.g. Grails'
Validateable). On Groovy 6.0 the dispatch shape that exposes this — trait-body calls seeing the implementer’s override — is opt-in via@Virtual; the default for public trait statics is declarer-bound with the method promoted onto the trait interface as a JVM-native interface static. The full dispatch behaviour is defined in Static members. -
Runtime trait application (the
as/withTraitsform) is unique to Groovy. None of the other three constructs offers a way to attach a trait to an existing instance at runtime. -
Self-types are first-class in Scala, an annotation in Groovy, and absent from Kotlin and Java. The Groovy and Scala forms enable statically checked traits whose bodies depend on members supplied by the implementing class without resorting to defensive casts.
Cross-version evolution
| Version | Year | Change |
|---|---|---|
2.3.0 |
2014 |
Initial release of traits. |
2.4.0 |
2015 |
|
3.0 |
2020 |
Default methods declared in |
4.0 |
2022 |
Sealed traits supported via |
5.0 [Interim] |
2024 |
Default, private and static methods in |
5.0.7 / 6.0.0-alpha-2 |
2026 |
Trait-body |
6.0.0-alpha-2 |
2026 |
Public trait static methods are declarer-bound by default
(GROOVY-12093) and are promoted onto the generated trait interface as
JVM-native interface statics (GROOVY-12111), so both |
Non-goals and potential future extensions
The following items are deliberately out of scope for this GEP and for the current implementation. Any of them would warrant a follow-up revision of this document or a successor GEP.
-
Promoting trait static-member support out of incubating status. The current per-implementer template semantics is a deliberate JVM-shaped compromise; a "true" shared-static-member model would require either breaking that semantics or moving static state into a separate runtime holder. Remaining open items in trait static-member dispatch — closure call-site parity and mixed-static-vs-instance verdicts — may be addressed in a successor revision.
-
Emitting trait methods as native interface default methods on JDK 8+. The four-class helper model deliberately avoids this so that Java callers see traits as plain interfaces. Switching would be a Java-interop change.
-
First-class compatibility guarantees for arbitrary AST transforms on traits.
-
Constructor support on traits.
-
protectedor package-private trait methods.
References and useful links
-
Schärli, Ducasse, Nierstrasz, Black. Traits: Composable Units of Behaviour. ECOOP 2003. — the academic foundation.
-
Scala traits — the closest sibling construct in another JVM language.
-
JEP 126: Default Methods — Java’s stateless analogue, contrasted in the Motivation section.
-
GEP-12: SAM coercion — underlies trait SAM coercion.
-
GEP-13: Sealed classes — applies to traits (sealed traits).
-
Cédric Champeau, Rethinking API design with traits — original design motivation and worked examples.
-
The language specification chapter on traits in the Groovy documentation contains worked tutorial examples that complement this spec-only document.
Reference implementation
Package: org.codehaus.groovy.transform.trait
-
TraitASTTransformation— entry point invoked for every trait declaration; produces the interface, helper, field-helper and (where applicable) static-field-helper artefacts. -
TraitComposer— weaves trait methods, accessors and bridge methods into each implementing class. -
TraitReceiverTransformer— rewrites references inside trait method bodies so thatthis,superand field accesses resolve to the implementing-instance receiver and to helper-routed state. -
Traits— utility class holding helper-class naming constants ($Trait$Helper,$Trait$FieldHelper,$Trait$StaticFieldHelper), trait detection predicates, and self-type collection logic. -
TraitTypeCheckingExtension— integrates traits with the static type checker (GEP-8).
Public API:
-
groovy.transform.Trait(since 2.3.0) -
groovy.transform.SelfType(since 2.4.0)
Representative JIRA issues
-
GROOVY-7134: introduce
@SelfTypefor statically checked traits. -
GROOVY-8233: Java stub generation for static properties of traits.
-
GROOVY-8299: native default/private/static methods in interfaces (decoupling from trait machinery in 5.0).
-
GROOVY-8219, GROOVY-8788:
@TupleConstructorinteraction with traits and default values. -
GROOVY-8951: trait getter conflicts with generated getter (pre-compiled case).
-
GROOVY-11674: deterministic ordering of trait methods for reproducible builds.
-
GROOVY-11907: trait field reference transform restructure.
-
GROOVY-8854 (interim): trait
this.staticMethod()receiver rewrite (the change that later caused the 5.0 dispatch regression corrected in GROOVY-11985). -
GROOVY-11985 (interim): experimented with restoring the 4.x behavior but ultimately rolled back.
-
GROOVY-12093: finalises the trait-static dispatch model — declarer-bound by default, with the
@Virtualmarker opting into per-implementer override dispatch for callees that need it. -
GROOVY-12111: exposes public trait static methods on the generated trait interface as JVM-native interface statics, so both
Trait.m()from outside the trait andT.m()from inside the trait body resolve at the JVM level (@Virtualmethods are excluded — interface statics are declarer-bound by JVM rule, incompatible with virtual dispatch). -
GROOVY-12104: rejects the
T.this.*qualifier inside trait code at compile time (previously produced invalid or mis-typed bytecode at runtime). -
GROOVY-12105: rejects unqualified
super.m(…)from astatictrait method at compile time (previously threwMissingMethodExceptionbecause the trait chain is not walked for statics). -
GROOVY-12106: a sub-trait again resolves a
staticmethod inherited from a super-trait from its own body, including with a subtype argument (static type checking had matched the inherited helper static by exact parameter type only, regressing this in the 5.0.x line). -
GROOVY-12112: rejects a qualified
Trait.m(…)call to a@Virtualtrait static with a clear compile-time error (under@CompileStatic/@TypeChecked) instead of the generic "cannot find matching method"; dynamic code still reports a runtimeMissingMethodException.
Update history
1 (2026-05-06) Initial draft. Retrospective specification capturing trait semantics as shipped in 2.3.0 and refined through 5.0, the four-class bytecode model, and the cross-version evolution table.
2 (2026-05-11) Added Comparison with related constructs section
contrasting Groovy traits with Scala traits, Kotlin interfaces, and
Java default methods across state, conflict resolution, stackable
super, static members, self-types, runtime application, and the
Java-callable view.
3 (2026-06-22) Rewrote Static members to describe the dispatch
behaviour shipping in Groovy 4.0.32 normatively (override visibility
from trait-body call sites, closure call-site asymmetry, private-static
trait-anchoring, per-direct-implementer composition, static-field
template binding with a worked example of the accessor pattern that
expresses per-implementer overridable defaults, T.m() unsupported
from trait code, the uniform trait-shadows-superclass rule across the
static/instance quadrant, and the T.super.m() disambiguation escape).
Corrected the @CompileStatic claim. Added cross-version entries for
the GROOVY-8854 regression and the GROOVY-11985 restoration shipping
in 5.0.7 / 6.0.0-alpha-2. Cross-referenced GROOVY-12093 for the remaining
static-dispatch design work.
4 (2026-06-27) From 5.0.7. Added @Virtual, a marker for
public static trait methods that opts into per-implementer override dispatch:
trait-body calls to a @Virtual callee are routed through the
implementing class at runtime, so a same-signature static on the
implementer shadows the trait’s default from inside trait code. Valid
only on public, non-abstract static trait methods; applying it
elsewhere is a compile-time error.
5 (2026-06-28) From 6.0.0. Public trait static methods are now promoted onto
the generated trait interface as JVM-native interface statics by default
(GROOVY-12111),
so both Trait.m() from outside the trait and T.m() from inside
the trait body resolve at the JVM level. @Virtual methods are
excluded from the promotion (interface statics are declarer-bound by
JVM rule, incompatible with virtual dispatch). Trait-body qualifier
syntax tightened in the same revision: T.this.* is rejected at
compile time (GROOVY-12104; previously produced a VerifyError on 4.x
and a ClassCastException on 5.x/6.x) and unqualified super.m(…)
from a static trait method is rejected at compile time
(GROOVY-12105; previously threw MissingMethodException because the
trait chain is not walked for statics). Item 2 of this, super, and
stackable traits now cross-references the static-method restriction;
the recommended alternatives are existing documented
features (this.m(…) for normal dispatch and T.super.m(…) for
explicit trait-anchored dispatch).
6 (2026-06-28) From 6.0.0. Documented in Static members that a
sub-trait resolves a static method inherited from a super-trait from
its own body by the same rules as a static it declares itself —
unqualified/this. (declarer-bound, or override-visible under
@Virtual), Parent.m(…) (via the promoted interface static), and
Parent.super.m(…) — with subtype-aware argument resolution, matching
plain-class static inheritance (GROOVY-12106; this had regressed in the
5.0.x line). Added the corresponding cross-version and JIRA-issue entries.
7 (2026-06-30) Added
a "When to use @Virtual" rationale to Static
members (template-method-like overridable static defaults),
a phase framing (defining vs weaving vs using a trait, and
the @CompileStatic compile-error vs dynamic runtime-error split),
a Generics section that explains the similarities and differences
compared to normal classes, and
a Metaprogramming section that explains the similarities and differences
compared to normal classes.