Annotation Interface ClassTag


Parameter annotation marking a Class parameter as compiler-supplied under static compilation. On the JVM, a method that needs the runtime type of a generic type parameter cannot recover it (erasure), so the API must take an explicit Class<T> token. That token is redundant with a type the compiler already knows — the receiver's type argument. @ClassTag removes the redundancy.

Under @CompileStatic / @TypeChecked, when a call omits the Class token argument(s), the type checker may select an overload whose parameters, beyond those matching the supplied arguments, are all @ClassTag Class<X> and synthesise the X.class argument(s) from the receiver's matching type argument(s), rewriting the call. Each @ClassTag parameter is filled at its declared position — anywhere in the parameter list, not just trailing — and the caller's supplied arguments map to the remaining (non-tag) parameters in order.

As an example, asChecked declares an explicit token:

 public static <T> List<T> asChecked(List<T> self, @ClassTag Class<T> type)
 
which lets statically-typed callers omit it:
 List<String> list = [].asChecked()      // compiler injects String.class
 

Selection is normally additive: the tagged overload is chosen only when the call as written matches no method (as above — there is no token-less asChecked()). When a token-less overload also matches, choosing the tagged one instead (preemption) changes the meaning of existing source, so three conditions gate it:

  • Intent — the API author must declare the tagged overload preemptive with preempt=true; without it the overload is only ever selected additively. Groovy's own checked withDefault overloads declare it (their lenient sibling predates them and could silently corrupt a typed map's key set).
  • Containment — a tagged overload may only preempt a token-less overload declared by the same class (or, for instance methods, the same class hierarchy). A library can upgrade callers of its own lenient API but can never capture calls owned by another library, so adding a jar to the compile classpath cannot re-route existing calls it does not already own.
  • Consent — the consumer may veto all preemption globally via CompilerConfiguration.setClassTagPreemptionDisabled(boolean) (also seeded from the groovy.classtag.preemption.disable system property). Code compiled with the veto behaves exactly as it did before any library declared preemption; a finer-grained (per-method or per-call-site) opt-out may be added later if experience shows the need.

Boundaries:

  • Static only. Dynamic Groovy ignores the annotation and binds the original overload; the shorter (token-less) spelling is static-only syntax. If no token can be synthesised there is no zero-token method to bind, so the call fails to resolve.
  • The type variable must be statically known. List<String> works; def, raw List, and wildcard-only types do not — no token is synthesised.
  • Receiver type arguments only. A type variable declared by the method itself (which shadows any like-named class variable) cannot be reified from the receiver; tagging one on an instance method compiled from source is a compile-time error. Static methods are exempt: in the extension-method authoring pattern the type variable is necessarily method-declared and connects to the receiver through the self parameter.
  • Erased fidelity. The synthesised Class<X> reifies only the erased class: List<String> and List<Integer> are indistinguishable.
  • Nothing-to-gain guard. When every token would erase to Object (e.g. a List<Object> receiver), no token is synthesised — a checked Object view could never reject anything — so an additive call fails to resolve and a preemptive upgrade is skipped (the lenient overload is kept).
  • Escape hatch. Passing the Class explicitly always works, in every mode.
Since:
6.0.0
  • Optional Element Summary

    Optional Elements
    Modifier and Type
    Optional Element
    Description
    boolean
    Declares that the overload carrying this parameter may preempt a matching token-less overload declared by the same class (or class hierarchy): an existing call that binds the token-less overload is transparently upgraded to this one under static compilation.
    Optionally names the type variable to reify, for when the parameter type cannot carry it (e.g.
  • Element Details

    • value

      String value
      Optionally names the type variable to reify, for when the parameter type cannot carry it (e.g. a raw or wildcard Class token, or a method whose own type variables do not line up with the receiver's). When empty (the default), the type variable is read from the parameter type Class<X>.

      Note: for is a Java reserved word and cannot be an annotation member name, so the override is spelled @ClassTag("K") (or @ClassTag(value = "K")). For a method compiled from source in the same unit, the static type checker rejects a name that does not resolve to a type variable in scope, so typos fail at compile time. The check cannot reach an extension method supplied by an already-compiled library (it is never visited as source); such a typo silently disables injection rather than being reported.

      Returns:
      the name of the type variable to reify, or empty to read it from the parameter type
      Default:
      ""
    • preempt

      boolean preempt
      Declares that the overload carrying this parameter may preempt a matching token-less overload declared by the same class (or class hierarchy): an existing call that binds the token-less overload is transparently upgraded to this one under static compilation. Because that changes the meaning of existing source, it is off by default; without it the overload is only selected additively (when the call as written matches no method). Consumers retain a veto via CompilerConfiguration.setClassTagPreemptionDisabled(boolean). By convention, set it on every @ClassTag parameter of the overload (any one suffices to declare the intent).
      Returns:
      whether this overload may preempt its same-owner token-less sibling
      Default:
      false