Java Generic Multiple Type Parameters: Syntax and Usage
Learn how to use multiple type parameters in Java generic classes and methods, including type bounds, inference, wildcards, and type erasure.
Java generics support multiple type parameters, allowing a class or method to operate on several independent types. This is common in types such as Map<K, V> or custom data structures that pair two values. The sections below cover the syntax, type bounds, inference, wildcards, and erasure behavior you need when using multiple type parameters.
Declaring a Class with Multiple Type Parameters
To declare a class with multiple type parameters, list the type parameters inside angle brackets after the class name, separated by commas. For example, a simple Pair class that holds two values of different types:
public class Pair<K, V> { private final K key; private final V value; public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }
Here, K and V are type parameters. When you instantiate this class, you provide concrete types for each parameter. The compiler enforces that the types are consistent, so you cannot directly assign a Pair<String, Integer> to a variable of type Pair<String, String>. A cast would be an unchecked cast and could fail later.
The number of type parameters is not limited to two. You can use as many as your design requires, though more than a few usually indicate that a different abstraction might be cleaner. For example, a Triple class could have three parameters, but in practice a record or a dedicated class is often clearer.
Using Multiple Type Parameters in Methods
Methods can also declare their own type parameters, independent of the class-level parameters. This is useful for utility methods that operate on two types without belonging to a class that is parameterized. For example:
public static <T, U> boolean areEqual(T first, U second) { return first.equals(second); }
The type parameters <T, U> appear before the return type. This method accepts any two objects and checks equality. T and U are inferred from the arguments, so you rarely need to specify them explicitly. You can call areEqual("text", 42), and the compiler infers T as String and U as Integer.
When the type parameters are independent, the order has no semantic effect; <T, U> and <U, T> are equivalent. Keeping a consistent order improves code comprehension.
Type Bounds and Constraints on Multiple Parameters
Sometimes you need to restrict the types that can be used as type parameters. You can apply a bound to each parameter independently. For example, the following method requires one argument to be a Number and the other to be a CharSequence:
public static <N extends Number, C extends CharSequence> String format(N number, C text) { return text.toString() + ": " + number.doubleValue(); }
Here, N is bounded by Number, and C is bounded by CharSequence. The compiler enforces both bounds at the call site; passing a value outside the bound is a compile-time error.
Bounds can also use intersection types on a single parameter, such as <T extends Serializable & Comparable<T>>. The key point is that each type parameter can have its own bound, and the compiler enforces those bounds independently.
Type Inference and Diamond Operator with Multiple Parameters
When instantiating a generic class with multiple type parameters, you can use the diamond operator <> to let the compiler infer the types from the constructor arguments. For example:
Pair<String, Integer> pair = new Pair<>("key", 42);
The compiler infers K as String and V as Integer from the constructor call. This works for any number of type parameters as long as the constructor arguments provide enough information. If the constructor arguments are ambiguous or insufficient, you must specify the types explicitly.
For methods, type inference works similarly. The compiler uses the arguments to determine the type parameters. In some cases, you may need to provide an explicit type witness to disambiguate, especially when the method is called without arguments or when the return type is used in a generic context. For example:
Collections.<String, Integer>emptyMap();
This is rarely necessary in modern Java because the compiler can usually infer the type from the target type, but an explicit type witness is still available.
Wildcards and Multiple Type Parameters
Wildcards (?) are used when you want to accept a generic type without knowing its exact type parameters. With multiple type parameters, wildcards can be applied to each parameter independently. For example, a method that accepts any Pair regardless of its key and value types:
public static void printPair(Pair<?, ?> pair) { System.out.println(pair.getKey() + " = " + pair.getValue()); }
You can also use bounded wildcards, such as Pair<? extends Number, ? super Integer>, though that is less common. Wildcards are useful when you want to write code that is agnostic to the specific types, but you must be careful about the direction of data flow. With Pair<?, ?>, you can read the key and value as Object, but you cannot safely call a method that requires a specific concrete type.
Type Erasure and Compatibility Considerations
Java generics are implemented via type erasure. At runtime, the JVM does not know about the type parameters; they are erased to their bounds or to Object. This has several implications for multiple type parameters. First, you cannot check the type parameters at runtime. For example, pair instanceof Pair<String, Integer> is a compile-time error because the type arguments are not reifiable. Second, you cannot create arrays of parameterized types directly, such as new Pair<String, Integer>[10]. This is a common source of confusion.
Type erasure also affects overload resolution. Two methods that differ only in the type arguments used for the same generic class have the same signature after erasure. For example, you cannot have both void process(Pair<String, Integer> p) and void process(Pair<String, String> p) in the same class, because after erasure both become void process(Pair p). This is a key compatibility constraint when designing APIs with multiple type parameters.
Despite these limitations, generics provide compile-time safety. The compiler erases type parameters and inserts casts where necessary, and the type system checks the source code against the declared types.
Maintainability and Design Choices with Multiple Type Parameters
When designing a class or method with multiple type parameters, consider whether the abstraction is actually needed. Too many type parameters can make code harder to read and maintain. A good rule of thumb is to limit the number to two or three, and to use descriptive names like K for key and V for value when the roles are clear.
If you find yourself using many type parameters, consider whether a nested generic structure or a separate class would be clearer. For example, instead of Pair<Pair<A, B>, C>, you might define a dedicated Triple<A, B, C> class. Similarly, if the type parameters are related, a single parameter with a bound might be sufficient.
Another maintainability concern is the use of wildcards. Wildcards can make APIs more flexible but also harder to understand. Use them only when the flexibility is necessary, and document the intended usage.
Finally, remember that type parameters are erased, so any runtime behavior that depends on the specific type must be handled outside the generic class, often by passing a Class object or using a type token.