@Documented
@Incubating
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @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:
withDefault overloads declare it (their lenient sibling predates
them and could silently corrupt a typed map's key set).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:
List<String> works;
def, raw List, and wildcard-only types do not — no token is synthesised.Class<X> reifies only the erased class:
List<String> and List<Integer> are indistinguishable.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).Class explicitly always works, in every mode.| Type | Name and Description |
|---|---|
boolean |
preemptDeclares 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. |
String |
valueOptionally 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). |
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.
By convention, set it on every @ClassTag parameter of the overload (any one
suffices to declare the intent).
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.