How do you read a method or class's type variables and their bounds via reflection, and what do you get for `<T extends Number & Comparable<T>>`?
answer
- getTypeParameters() on Class/Method
- TypeVariable: getName/getBounds/getGenericDeclaration
- unbounded T -> [Object]
- multi-bound preserves declaration order
- first bound drives erasure
- bounds are Types (can be ParameterizedType, even self-referential)
basics
~20 sUse getTypeParameters() on a Class or Method to get its declared type variables (like T). Each TypeVariable has getBounds(), which returns its upper bounds — for <T extends Number & Comparable<T>> you get [Number, Comparable<T>].
solid answer
~40 sGeneric declarations expose their type parameters reflectively. Class.getTypeParameters() and Method.getTypeParameters() return a TypeVariable<?>[] for declarations like class Box<T> or <T> T pick(...). Each TypeVariable has getName() (e.g. 'T'), getGenericDeclaration() (the class/method that owns it), and getBounds(), returning the upper bounds as a Type[]. An unbounded T has a single bound of Object.class. A multi-bounded <T extends Number & Comparable<T>> returns [Number.class, ParameterizedType for Comparable<T>] in declaration order, where the first bound is the erasure-determining one. Bounds are themselves Types, so a bound like Comparable<T> is a ParameterizedType whose argument is the same TypeVariable. This is the same retained-signature mechanism behind getGenericType — declarations keep their generic signature, instances don't. It underpins generic type-resolution engines (Spring's ResolvableType, Guava's TypeToken) that substitute actual arguments for type variables.
code
java · 14 linesimport java.lang.reflect.*;
class Util {
static <T extends Number & Comparable<T>> void use(T t) {}
}
public class Demo {
public static void main(String[] args) throws Exception {
Method m = Util.class.getDeclaredMethod("use", Number.class);
TypeVariable<?> t = m.getTypeParameters()[0];
System.out.println(t.getName()); // T
for (Type b : t.getBounds()) System.out.println(b); // Number, then Comparable<T>
}
}go deeper
Aware that generic classes/methods have type parameters like T but typically doesn't reflect over them.
Can call getTypeParameters() and read a TypeVariable's name and single bound.
Handles multi-bounds and their order, knows unbounded means [Object], distinguishes type parameters from actual arguments, and recognizes self-referential bounds.
Designs or reasons about a full type-resolution engine (variable-to-argument substitution across the supertype graph), understands erasure's interaction with the first bound, and evaluates library choices (ResolvableType/TypeToken) for framework infrastructure.
## Type variables, briefly When you declare `class Box<T>` or a generic method `<T> T first(List<T> list)`, the `T` is a **type variable** (also called a type parameter): a placeholder for a type that callers supply. A type variable can have **bounds** — constraints on what `T` may be — written with `extends`: `<T extends Number>` means T must be Number or a subtype. You can combine bounds with `&`: `<T extends Number & Comparable<T>>` means T must be a subtype of Number *and* implement Comparable<T>. ## Reading them reflectively The generic-signature metadata that the compiler stores (the same *Signature* attribute that lets `getGenericType` work) also records type-variable declarations. The entry points: - `Class.getTypeParameters()` → `TypeVariable<Class<T>>[]` for the class's own type parameters. - `Method.getTypeParameters()` and `Constructor.getTypeParameters()` → the method's/constructor's own type parameters. These return an empty array for non-generic declarations. ## The TypeVariable interface Each element is a `java.lang.reflect.TypeVariable<D>` (a sub-interface of `Type`) with: - `getName()` → the source name, e.g. `"T"`. - `getGenericDeclaration()` → the `GenericDeclaration` (a `Class`, `Method`, or `Constructor`) that declared it. This is how you know whether a `T` belongs to the class or to a method. - `getBounds()` → a `Type[]` of the **upper bounds**. - `getAnnotatedBounds()` → the bounds with type-use annotations (Java 8+ type annotations). ## What getBounds() returns in detail - **Unbounded** `<T>`: `getBounds()` returns a single-element array `[Object.class]`. There is always at least one bound; the implicit one is `Object`. - **Single bound** `<T extends Number>`: returns `[Number.class]`. - **Multiple bounds** `<T extends Number & Comparable<T>>`: returns `[Number.class, <ParameterizedType for Comparable<T>>]`, in the order written. The **first** bound is special: it determines the *erasure* of T (here `Number`), and if you want a class (not an interface) as the first bound it must come first syntactically. - A bound that is itself generic (like `Comparable<T>`) comes back as a `ParameterizedType`; calling `getActualTypeArguments()[0]` on it yields the **same `TypeVariable` T** — bounds can be self-referential (F-bounded polymorphism, the classic `Enum<E extends Enum<E>>`). ## How this connects to the broader picture Type variables, parameterized types, wildcards, and generic arrays are the four non-`Class` shapes of `java.lang.reflect.Type`. A general type-walking routine has to `instanceof`-dispatch on all of them. Resolving a `TypeVariable` to a concrete type requires knowing the actual arguments at some use site — e.g. given `IntBox extends Box<Integer>`, you read `Box`'s type parameter `T` and the `ParameterizedType Box<Integer>` from `IntBox.getGenericSuperclass()`, then map `T → Integer`. Libraries (Spring `ResolvableType`, Guava `TypeToken`, Jackson `TypeFactory`) automate exactly this substitution. The reason any of it is possible is the same as for generic fields/returns: **declarations retain their generic signature even though instances are erased.** ```java Method m = ...; // <T extends Number & Comparable<T>> void use(T t) TypeVariable<?> t = m.getTypeParameters()[0]; Type[] bounds = t.getBounds(); // [Number, Comparable<T>] ```
- What does getBounds() return for a completely unbounded type variable <T>?A single-element array containing Object.class — there is always at least one (implicit) upper bound.
- How would you resolve T to Integer given class IntBox extends Box<Integer>?Read Box.getTypeParameters() to get T, read IntBox.getGenericSuperclass() as a ParameterizedType (Box<Integer>), then map T to its actual type argument Integer.class.
saying these in an interview costs you the question
- Thinking an unbounded T has zero bounds (it has Object)
- Expecting getBounds() to give the actual argument at a call site (it gives constraints, not bindings)
- Confusing getTypeParameters (declared variables) with getActualTypeArguments (supplied arguments)
- Assuming type-variable resolution is automatic — you must map declarations to a ParameterizedType use site