ArchitectMind
  1. Home
  2. Tutorials
  3. Book 1 · Core Java & Modern Java
  4. Volume II
  5. Type Parameters
Book 1 · Volume II — Generics, Collections & Type System · Topic 1.2

Type Parameters

Type parameter declaration, multiple type parameters and naming conventions

7 min readStage 1 · Foundations4 interview questions
Prerequisites
1.1 Generic Types
Covers
Type parameter declaration · Multiple type parameters · Naming conventions
Related
2.1 Bounded Type Parameters · 2.4 Type Inference
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally
  5. 5. APIs / Features / Syntax
  6. 6. Practical Examples
  7. 7. Common Problems & Mistakes
  8. 8. Performance & Production Considerations
  9. 9. Architecture & Design Considerations
  10. 10. Interview Questions & Answers
  11. Key Takeaways

1Concept / Definition

A type parameter is a name introduced in a generic declaration that stands for a type to be supplied later. It is declared in angle brackets after a class or interface name, or before a method's return type, and can be used wherever a type is expected within its scope, subject to the restrictions of erasure (3.3).

Type parameters are what make a declaration generic. Without them, one class or method cannot express that its input and output types are related, for example that get() returns the same type that put() accepted.

2Why It Exists

Before generics, reusable code worked with Object. A Map accepted any key and any value, and every read needed a cast such as (Order) map.get(id). A wrong cast compiled cleanly and failed at run time with a ClassCastException, often far from the code that inserted the wrong object.

Type parameters let a declaration name the types it works with and state how they relate. Map<K, V> says that keys are all one type and values all another; Function<T, R> says the function takes a T and returns an R. The compiler checks every use against these names, inserts the casts itself and reports mistakes at compile time, so one implementation can be reused safely for any type.

3Core Concepts

Type parameter declaration

A declaration lists one or more names, each optionally followed by a bound: <T>, <T extends Number> (2.1). A class's type parameters are in scope for all instance members but not for static members, because static members are shared by all parameterisations. A method's type parameters are in scope only within that method.

Multiple type parameters

Several parameters are separated by commas: Map<K, V>, Function<T, R>, BiFunction<T, U, R>. Each is independent; one may be used in the bound of another (<K, V extends Comparable<V>>).

Declaring and using multiple type parameters
Figure 1. — Declaring and using multiple type parameters

Naming conventions

By convention type parameters are single uppercase letters, which distinguishes them from class names at a glance. The JDK uses a consistent set:

Standard type parameter names
Figure 2. — Standard type parameter names

Longer names (<ENTITY>, <TResult>) are occasionally used in complex frameworks, but single letters remain the norm; readability comes from keeping the number of parameters small.

4How It Works Internally

Scope of a type parameter

A class's type parameters are in scope in its instance fields, constructors, instance methods and non-static nested classes. They are not in scope in static members, because a static field or method is shared by every parameterisation of the class: Cache<String, Order> and Cache<Long, User> share the same static state, so K has no single meaning there. A method's type parameters are visible only inside that method, and a constructor can declare its own in the same way.

Erasure of type parameters

After type-checking, javac erases each type parameter to its leftmost bound: <T> becomes Object, <T extends Number> becomes Number, and <T extends Number & Comparable<T>> becomes Number. The compiler inserts casts where values come back out, so the bytecode works with the erased types. Because of this, a type parameter cannot be used where a run-time type is needed: new T(), T.class, new T[10] and x instanceof T do not compile.

What reflection can still see

The declarations survive in the class file's Signature attribute, so reflection can report them. Pair.class.getTypeParameters() returns TypeVariable objects named A and B, each with its bounds. What reflection cannot recover is the type argument used by a particular object, because an instance of Pair<String, Integer> and one of Pair<Order, Long> are the same class at run time.

5APIs / Features / Syntax

Type parameters can be declared on classes, interfaces, records, methods and constructors. A method declares them before the return type, and callers can usually let the compiler infer them or, when inference needs help, supply them explicitly.

Declaration forms
// class, interface and record
public class Box<T> { private T value; }
public interface Converter<S, T> { T convert(S source); }
public record Pair<A, B>(A first, B second) { }

// generic method: type parameter before the return type
public static <T> List<T> listOf(T first, T second) { return List.of(first, second); }

// bounds: one class first, then interfaces, joined with &
public static <T extends Number & Comparable<T>> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; }

// explicit type argument when inference needs help
List<String> names = Collections.<String>emptyList();

Recursive bounds such as <T extends Comparable<T>> say that T can be compared with other values of the same type. They are common in sorting and ordering utilities and in builder APIs that return the concrete builder type.

6Practical Examples

Multiple parameters and scope
public record Pair<A, B>(A first, B second) {
    public <C> Pair<A, C> withSecond(C newSecond) {  // method adds its own C
        return new Pair<>(first, newSecond);
    }
}

public class Cache<K, V> {
    private final Map<K, V> store = new ConcurrentHashMap<>();
    public V get(K key, Function<? super K, ? extends V> loader) {
        return store.computeIfAbsent(key, loader);
    }
    // static <K> void bad(K k) { }   // fine: this K is a new method parameter,
    // static K shared;  // error: class K not usable in static context
}

The Pair record uses two class type parameters, A and B, while withSecond declares its own C, so it can return a pair whose second element has a different type. Cache<K, V> keeps keys and values related: a Cache<String, Order> can only store orders under string keys, and get returns an Order without a cast.

Example 2 — Shadowing the class parameter

A common slip is to declare a method type parameter with the same name as the class's. The method's T then hides the class's T, and the two are unrelated even though they look the same.

Shadowing T
public class Box<T> {
    private T value;

    // This <T> is a NEW type parameter that hides the class's T
    public <T> void set(T newValue) {
        // value = newValue;   // does not compile: the two Ts are different types
    }

    // Correct: use the class's T without redeclaring it
    public void put(T newValue) { this.value = newValue; }
}

Remove the method-level declaration, or give a genuinely different parameter a different name such as U. Most IDEs warn about this shadowing.

Example 3 — A recursive bound

Recursive bound
public static <T extends Comparable<T>> T largest(List<T> items) {
    T best = items.getFirst();
    for (T item : items) {
        if (item.compareTo(best) > 0) best = item;
    }
    return best;
}

largest(List.of(3, 9, 4));          // 9: Integer implements Comparable<Integer>
largest(List.of("pear", "apple")); // pear
// largest(List.of(new Object()));  // does not compile: Object is not Comparable

The bound makes compareTo available on T and rejects types that cannot be ordered at compile time, rather than failing at run time.

7Common Problems & Mistakes

Using the class's type parameter in a static member. static T defaultValue; does not compile. Pass the type in as a method type parameter or store a Class<T> token in an instance field instead.

Expecting the type to exist at run time. new T() and new T[n] fail because of erasure. Ask the caller for a Supplier<T> or a Class<T>, for example create(Supplier<T> factory).

Shadowing. A generic method in a generic class that reuses the name T introduces a second, unrelated type, as Example 2 shows.

Too many or unclear parameters. Processor<A, B, C, D> is hard to read and to infer. Group related types into a named type, or fix some of them in a subclass such as OrderProcessor implements Processor<Order, Receipt>.

Raw types. Writing Pair p instead of Pair<String, Integer> switches off type-checking for that variable and produces unchecked warnings that hide real mistakes.

8Performance & Production Considerations

Type parameters cost nothing at run time. After erasure the bytecode is the same as hand-written code that uses Object and casts, and the JIT compiler removes most of those casts. The real costs are elsewhere: type arguments must be reference types, so List<Integer> boxes every number, which matters in large collections and hot loops; and complex generic signatures slow down humans more than machines, because compiler error messages about inference failures can be long and hard to read.

9Architecture & Design Considerations

In API design, every type parameter is part of the contract and is hard to remove later. Add one only when it expresses a real relationship between types that callers benefit from. Infrastructure code, such as repositories, caches, result wrappers and event envelopes, gains the most, because one tested implementation then serves every domain type. Domain classes usually gain little: Order and Invoice read better as concrete types than as Document<T>.

Keep public signatures to one to three type parameters with conventional names. When a parameter is fixed in practice, fix it in a subtype. When a method needs the run-time type, design the API to receive it explicitly, through a Class<T> or a type reference, rather than hiding reflection inside the library.

10Interview Questions & Answers

Q1KnowledgeConcept

What do the type parameter names E, K, V, T and R conventionally mean?

These are naming conventions used throughout the JDK and most Java libraries, not language rules. E stands for the element type of a collection, as in List<E> or Set<E>. K and V stand for the key and value types of a map, as in Map<K, V>. T stands for a general type, with U, V and S used for a second, third or fourth type when needed. R stands for the result or return type of a function, as in Function<T, R>. Using single uppercase letters makes type parameters easy to tell apart from class names, and following the convention means readers understand a signature at a glance.

Q2UnderstandingTricky

Can a static method or field use the class's type parameter?

No. A static field or method belongs to the class itself and is shared by every parameterisation, so Cache<String, Order> and Cache<Long, User> share the same static members and the class's K or V has no single meaning there. Writing static T value or using T in a static method body does not compile. A static method can declare its own type parameter instead, as in static <T> Box<T> of(T value), which is how factory methods such as List.of work. If static code needs a type, pass it in as a method type parameter or as a Class<T> argument.

Q3ApplicationCode

Can one type parameter be used in the bound of another?

Yes. Type parameters declared in the same section can refer to each other in their bounds, and the order does not matter. For example, <K, V extends Comparable<V>> requires the value type to be comparable with itself, and <T, C extends Collection<T>> requires C to be a collection of T, so a method can accept and return the same kind of collection. A parameter can even refer to itself, as in the recursive bound <T extends Comparable<T>>. These dependent bounds let a signature express precise relationships that the compiler then checks at every call site.

Q4Senior EngineerDesign

How many type parameters are too many for a public API?

There is no fixed limit, but beyond two or three type parameters a public API becomes hard to read, document and use, and the compiler's type inference and error messages become harder to follow. Many parameters often mean the abstraction is doing too much, or that some types are always the same in practice. Options are to fix those types in a subtype, such as OrderHandler implements Handler<Order, Receipt>, to group related types into a single named type, or to split the API into smaller pieces. The JDK itself rarely goes beyond three, as in BiFunction<T, U, R>.

Key Takeaways

  • Type parameters are declared in angle brackets, optionally with bounds.
  • Class parameters are not visible in static members; method parameters are local to the method.
  • Use conventional single-letter names: T, E, K, V, R.

Coverage checklist

Type parameter declaration ✔ · Multiple type parameters ✔ · Naming conventions ✔

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.