GEP-22


Metadata
Number

GEP-22

Title

Traits

Version

7

Type

Feature

Status

Final

Comment

Reflects trait semantics as of Groovy 6

Leader

Cédric Champeau

Created

2026-05-06

Last modification

2026-06-30

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 @Mixin annotation, since deprecated) weave behaviour at runtime but the resulting object is not an instanceof the mixed-in type, defeating type-based dispatch and compile-time checks.

  • Delegation via @Delegate expresses 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. protected and 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 final modifier records the intended modifier of the woven method in the implementing class. Mixing final and non-final declarations 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 bar of type T declared in trait my.pkg.Foo, the implementing class gains a field named my_pkg_Foo__bar of type T.

  • 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:

  1. this refers 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.

  2. An unqualified super.m(…​) call delegates to the next trait in the implementation chain (in the order produced by the implements clause of the implementing class, walked right-to-left). When the chain is exhausted, super resolves to the actual superclass of the implementing class. (This applies to instance methods only; from a static trait method it is rejected — see the unsupported-syntax list below.)

  3. A qualified T.super.m(…​) call resolves to trait T’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.this is Java’s enclosing-instance access for inner classes, and a trait is not an enclosing scope of its implementer (per item 1, this is 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 use this.m(…​) (item 1); for explicit trait-anchored dispatch use T.super.m(…​) (item 3).

  • unqualified super.m(…​) from a static trait method (GROOVY-12105). The unqualified super form of item 2 is supported for instance methods only; from a static trait method the trait chain is not walked. Use T.super.m(…​) for the trait’s own static m, or call the implementer-side method by name without a qualifier. (This restriction is specific to traits: Groovy permits unqualified super.m(…​) in static methods 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 trait T.

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 (or TraitA and TraitB),

  • 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, a T.this.* qualifier, or unqualified super from 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 implements clause: 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/@TypeChecked an unresolvable call is a compile-time error; in dynamic code the same call instead fails at runtime with MissingMethodException. 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 and Impl.staticMethod(…​) against an implementing class — are supported.

  • Static trait methods, fields and properties may be declared in, and called from, @CompileStatic and @TypeChecked contexts. 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(…​) or this.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; external Impl.m() reaches that method, but trait-body calls do not see it.

  • A public static method may be annotated @Virtual to opt into per-implementer override dispatch. Trait-body calls of the form m(…​) or this.m(…​) to a @Virtual callee 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 @Virtual method 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 qualified Trait.m(…​) form: a bare Trait.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 runtime MissingMethodException in dynamic code. Read a concrete class’s value with Impl.m(…​), or use T.super.m(…​) from within trait code for the trait’s own definition.

  • @Virtual is 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 @Virtual accessor 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 reference origin resolves through Groovy’s getter chain to getOrigin(), which is @Virtual and so dispatches through the implementing class, picking up the override.

  • Trait composition occurs at the class that names the trait in its implements clause. 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 trait T'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 any Trait.m() caller sees from outside the trait. This applies to plain (non-@Virtual) statics only; a @Virtual callee has no qualified form, so T.m() from trait code is rejected the same way as any other Trait.m(…​) call (GROOVY-12112) — use unqualified m(…​)/this.m(…​) for override-visible dispatch, or T.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(…​) or this.m(…​) call is declarer-bound (or, for a @Virtual callee, dispatches through the implementing class so an implementer’s override is visible); a Parent.m(…​) call resolves through the super-trait’s promoted interface static (for the non-@Virtual statics that are promoted there); and Parent.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 m on the implementing class and from its body invoke super.m() for the superclass version and T.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

public static m()

Dispatches to the trait’s own copy (declarer-bound)

Both Trait.m() and Impl.m() resolve at the JVM level via the trait interface’s static

No

Mark @Virtual to opt into per-implementer override dispatch

@Virtual public static m()

Dispatches through the implementing class

Impl.m() works; Trait.m() is rejected (not on the interface) — compile error under @CompileStatic (GROOVY-12112), runtime MissingMethodException dynamically

Yes

T.super.m() from an override on the implementer reaches the trait’s copy

private static m()

Dispatches to the trait’s own copy (trait-internal)

Not visible

No (not overridable)

static x (bare field)

Reads the trait’s template field

Impl.x and Impl.@x each read that class’s own static field

No

Use a static accessor pair instead — see the worked example above

@Virtual static getX() / static setX() accessor pair

Property syntax x resolves through the getter chain

Impl.x resolves via the MOP

Yes (the @Virtual getter dispatches through the implementing class)

(the supported pattern for overridable static defaults)

Public instance method

Dispatches polymorphically

new Impl().m()

Yes

T.super.m() reaches the trait’s copy from any override

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:

  • An instance method would give override-visibility for free, but it is the wrong shape — the value answers a question about the type, and it is often needed where no instance exists yet (class initialisation, static factories, metadata building). You should not have to instantiate a class to ask a question about the class.

  • A plain static field is per-implementer, but a bare static-field read from trait code reads the trait’s own template field, not the implementer’s override (the documented non-goal above), so it cannot carry an overridable default that the trait honours.

  • An abstract / required contract removes the default entirely — every implementer is then forced to supply the value, losing the "sensible default, occasionally overridden" ergonomics.

@Virtual is the only construct that provides all three at once: a default, a per-class override, and trait-code visibility of that override, in a static context.

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:

  • @SelfType constrains what an implementing class must already be (its supertype shape).

  • @Sealed constrains 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 static method 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 @CompileStatic implementer code sees the reified element type.

  • A static method 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 stackable T.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, propertyMissing and invokeMethod; 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 of GroovyInterceptable, 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 implements GroovyInterceptable.

  • A trait method is an ordinary instance method on the implementer, so a runtime metaClass/ExpandoMetaClass override 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:

  1. An interface pkg.T containing the abstract and public-method signatures, plus accessor signatures for any properties. This is the type that participates in instanceof, generics, and Java interop. Public (non-@Virtual) static trait methods are also emitted here as JVM-native static interface methods that forward to the helper (GROOVY-12111); this is what makes both external Trait.m() and from-trait T.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+.

  2. A trait helper class named pkg.T$Trait$Helper (constant Traits.TRAIT_HELPER). Each non-abstract trait method m(args) is emitted as a static method 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.

  3. A field helper class named pkg.T$Trait$FieldHelper (constant Traits.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.

  4. A static field helper class named pkg.T$Trait$StaticFieldHelper (constant Traits.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.

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 val/var members; conflicts resolved by linearization.

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; @Virtual opts a static into per-implementer override dispatch and is then not promoted to the interface (so it has no qualified Trait.m(…​) form). See Static members.

Indirect, via a companion object. Static-like state lives on a singleton, not per implementer.

Indirect, via a companion object declared inside the interface. Members are instance members of the companion; @JvmStatic promotes them to true JVM statics.

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 object — one copy per trait.

Via the companion object — one copy per interface.

Yes — one copy per interface.

Constructors / parameters

Not permitted.

Scala 3 supports trait parameters (e.g. trait T(val name: String)); Scala 2 does not.

Not permitted.

Not permitted.

Method visibility

public and private only (also private static); protected and package-private are rejected.

Full range, including protected and qualified private[pkg].

public and private.

public, public static, and private (Java 9+).

Inherited-method conflict resolution

Last-wins by implements-clause order; override by re-declaring in the implementing class or via T.super.m().

C3 linearization — deterministic right-to-left walk of the inheritance graph; super[T].m() selects a specific ancestor.

Diamond is a compile error. The implementing class must resolve it explicitly via super<T>.foo().

Diamond is a compile error. The implementing class must resolve it explicitly via T.super.foo().

Stackable super

Yes. Unqualified super.m() walks the trait chain in implements order; T.super.m() jumps directly to trait T.

Yes. super.m() follows the linearization order; super[T].m() is the explicit form.

No. super requires a type qualifier wherever it is ambiguous; there is no walked chain.

No. T.super.m() is the only stack form; there is no chain to walk.

Self-types

@groovy.transform.SelfType(Foo.class) annotation; statically checked.

First-class syntax self: Foo => inside the trait body.

Not supported; expressed via bounded generics where unavoidable.

Not supported.

Runtime application to existing instances

subject as Trait and subject.withTraits(A, B) produce a new proxy that implements the trait(s) and delegates to the original.

Only at construction: new C with TraitA with TraitB. No way to attach a trait to an existing instance.

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 fun interface declarations.

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 default methods (since Scala 2.12).

A JVM interface; default-method generation is controlled by -Xjvm-default.

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 super is 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 / withTraits form) 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. trait keyword and @Trait annotation introduced; public/private methods; abstract methods; instance fields with name mangling; properties; multiple inheritance with last-wins conflict resolution; explicit T.super.m() resolution; stackable unqualified super; runtime application via as and Object.withTraits(…​); SAM coercion of single-abstract-method traits.

2.4.0

2015

@SelfType annotation introduced (GROOVY-7134), enabling statically checked traits whose method bodies depend on members supplied by the implementing class’s superclass hierarchy.

3.0

2020

Default methods declared in interface types using the Java 8 syntax were accepted by the parser and implemented under the hood by delegating to the trait machinery (incubating). No spec-level change to trait semantics.

4.0

2022

Sealed traits supported via @Sealed / the sealed keyword and the permits clause (see GEP-13). Distinction between @Sealed and @SelfType formalised in the spec. Java stub generation for static trait properties hardened.

5.0 [Interim]

2024

Default, private and static methods in interface types are now implemented as native JVM bytecode rather than via the trait machinery (GROOVY-8299). This decouples the interface-default-method feature from traits and improves Java interop. Multiple bug fixes landed for trait + @TupleConstructor default-value handling (GROOVY-8219, GROOVY-8788) and for trait static-field generation under static compilation (GROOVY-11817, GROOVY-11907). Note: GROOVY-8854 (Sep 2023) rewrote this.staticMethod() in trait bodies to bind through the trait helper rather than through the implementing class, disabling the previously available override behaviour (re-enabled using @Virtual as part of GROOVY-12093 - see below).

5.0.7 / 6.0.0-alpha-2

2026

Trait-body m() and this.m() calls to public trait statics can again dispatch through the implementing class at runtime, if using @Virtual (GROOVY-12093).

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 Trait.m() from outside the trait and T.m() from inside the trait body resolve at the JVM level. The @Virtual marker opts back into per-implementer override dispatch for callees that need it; @Virtual methods are not promoted onto the interface (interface statics are declarer-bound by JVM rule, incompatible with virtual dispatch). Misapplication of the marker (on instance, private, or abstract methods) is a compile-time error. The closure call-site asymmetry, the static-field non-goal, and the trait-shadows-superclass quadrant are all documented in Static members; no separate fork remains open at the time of writing. Trait-body qualifier syntax is tightened: T.this.* is rejected at compile time (GROOVY-12104; previously produced a VerifyError on 4.x and a ClassCastException at runtime 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). See this, super, and stackable traits. A sub-trait can again resolve a static method inherited from a super-trait from its own body, including when an argument’s static type is a subtype of the declared parameter type (GROOVY-12106; this had regressed in the 5.0.x line, where static type checking matched the inherited helper static by exact parameter type only). The fix is targeted for backport to the GROOVY_5_0_X line. A qualified Trait.m(…​) call to a @Virtual trait static — which has no implementing class to dispatch through — is now rejected with a clear, actionable message under @CompileStatic/@TypeChecked (GROOVY-12112) rather than the generic "cannot find matching method"; in dynamic code it remains a runtime MissingMethodException.

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.

  • protected or package-private trait methods.

  • 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 that this, super and 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 @SelfType for 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: @TupleConstructor interaction 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 @Virtual marker 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 and T.m() from inside the trait body resolve at the JVM level (@Virtual methods 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 a static trait method at compile time (previously threw MissingMethodException because the trait chain is not walked for statics).

  • GROOVY-12106: a sub-trait again resolves a static method 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 @Virtual trait static with a clear compile-time error (under @CompileStatic/@TypeChecked) instead of the generic "cannot find matching method"; dynamic code still reports a runtime MissingMethodException.

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.