Section10.9Bounded Type Parameters: Adding Constraints
Generic types and methods, as introduced in the previous sections (Box<T>, <E> void printArray(E[] arr)), provide excellent flexibility and type safety. However, by default, the type parameter (like T or E) is treated essentially as a placeholder for Object within the generic code. This means you can only reliably call methods defined in the Object class (like toString(), equals(), hashCode()) on variables of the type parameter.
But what if your generic algorithm or data structure needs to perform operations that are not defined in Object? Consider trying to write a generic method to find the larger of two items:
// Attempting a generic max method - THIS WON'T COMPILE!
public static <T> T findMaximum(T item1, T item2) {
// if (item1 > item2) { // Compile Error! The > operator is not defined for all Objects T.
// if (item1.compareTo(item2) > 0) { // Compile Error! Cannot assume T has compareTo().
// return item1;
// } else {
// return item2;
// }
return null; // Placeholder - actual comparison logic fails
}
This code fails to compile because the compiler has no guarantee that objects of an arbitrary type T can be compared using operators like > or even that they have a specific method like compareTo(). The T could be a String, a Player, or some other type where these operations aren’t defined.
To solve this, Java generics allow us to specify bounds on type parameters. A bounded type parameter restricts the kinds of types that can be used as type arguments for that parameter. This restriction acts as a contract, guaranteeing the compiler that any supplied type will have certain required methods or characteristics (i.e., it fulfills a specific contract beyond just being an Object).
Bounds are specified using the extends keyword after the type parameter declaration within the angle brackets. This might seem confusing initially because extends is also used for class inheritance, but in the context of generic type parameters, extends serves a broader role, meaning "is-a-subtype-of" or "implements", essentially indicating an "is-a" relationship or required capability.
// Bound with a class: T must be Number or a subclass of Number
<T extends Number>
// Bound with an interface: T must implement the Comparable interface
// Note: Comparable<T> itself is generic
<T extends Comparable<T>>
// Bound for a generic class definition
public class NumericBox<N extends Number> { /* ... */ }
// Bound for a generic method definition
public static <E extends Runnable> void runTask(E task) { /* ... */ }
If the bound is an interface (like Comparable or Runnable), the type argument must be a class that implements that interface (e.g., String implements Comparable<String>).
Subsection10.9.2Example 1: Bound with a Class (Number)
Let’s say we want a generic method that can inspect any kind of Number (Integer, Double, Float, etc.) and return its value as a double. We need to call the doubleValue() method, which is defined in the abstract Number class (the superclass for all standard numeric wrapper types). We use a bound to guarantee this method exists:
public class NumericUtils {
// Generic method constrained to Number types (or subtypes)
public static <N extends Number> double getDoubleValue(N number) {
// Because N extends Number, we are guaranteed the doubleValue() method exists!
// The compiler allows this call because of the bound.
return number.doubleValue();
}
public static void main(String[] args) {
Integer i = 10;
Double d = 25.5;
Float f = 3.14f; // Float also extends Number
System.out.println("Double value of Integer " + i + ": " + getDoubleValue(i));
System.out.println("Double value of Double " + d + ": " + getDoubleValue(d));
System.out.println("Double value of Float " + f + ": " + getDoubleValue(f));
// Now, let's try an invalid type:
String s = "hello";
// System.out.println(getDoubleValue(s)); // Compile Error!
// Error message might be:
// "Inferred type 'String' for type parameter 'N' is not within its bound;
// should extend 'java.lang.Number'"
}
}
The bound <N extends Number> ensures that only subtypes of Number can be passed to getDoubleValue. Inside the method, the compiler now knows that any object of type N will have the doubleValue() method available, making the call safe. Trying to call the method with a non-Number type like String results in a clear compile-time error, preserving type safety.
Subsection10.9.3Example 2: Bound with an Interface (Comparable)
Now let’s fix our earlier findMaximum method. To compare two objects using compareTo, they generally need to implement the Comparable interface. We use this interface as a bound:
import java.util.Arrays; // Needed just for List example below
public class ComparisonUtils {
// Generic method constrained to types that implement Comparable
// Note: Comparable itself is generic! Comparable<T> ensures type safety in comparison.
public static <T extends Comparable<T>> T findMaximum(T item1, T item2) {
// Because T extends Comparable<T>, we know compareTo(T) exists.
if (item1.compareTo(item2) > 0) {
return item1; // item1 is greater
} else {
return item2; // item2 is greater or they are equal
}
}
public static void main(String[] args) {
// String implements Comparable<String>
System.out.println("Max of 'apple', 'banana': " + findMaximum("apple", "banana"));
// Integer implements Comparable<Integer>
System.out.println("Max of 100, 50: " + findMaximum(100, 50)); // Autoboxing works
// Player class would need to implement Comparable<Player> for this to work:
Player p1 = new Player("Alice"); // Assume Player class exists BUT doesn't implement Comparable
Player p2 = new Player("Bob");
// findMaximum(p1, p2); // Compile Error!
// Error message might be:
// "Inferred type 'Player' for type parameter 'T' is not within its bound;
// should implement 'java.lang.Comparable<Player>'
// If Player implemented Comparable (see note below):
// Player maxPlayer = findMaximum(p1, p2); // This would then compile
}
}
Note10.9.1.Implementing Comparable (Example).
To make the findMaximum(p1, p2) call work, the Player class would need to implement the interface, like this minimal example:
public class Player implements Comparable<Player> {
private String name;
// Constructor and other methods...
public Player(String name) { this.name = name; } // Example constructor
@Override
public int compareTo(Player other) {
// Compare players based on their names (alphabetically)
return this.name.compareTo(other.name);
}
// toString() might be useful too
@Override public String toString() { return name; }
}
By implementing Comparable<Player> and providing the compareTo method, Player objects now satisfy the bound required by findMaximum.
The bound <T extends Comparable<T>> guarantees that any type T passed to this method will have a compareTo method that accepts another object of the same type T. This makes the call item1.compareTo(item2) safe at compile time. Attempting to use findMaximum with a type that doesn’t implement Comparable (like our original Player class) results in a compile error, as clearly shown in the example.
Note10.9.2.Self-Referential Bounds like Comparable<T>.
You might notice the bound uses Comparable<T>, where T refers back to the type parameter itself. This pattern is common for interfaces like Comparable to ensure type safety within the comparison method signature (i.e., you compare a String to another String, not to an Integer). The method signature int compareTo(T other) enforced by the bound guarantees this.
A type parameter can have multiple bounds, allowing you to require a type argument to satisfy several contracts simultaneously. There are specific rules for this:
// Syntax: Class bound (optional, max 1, must be first) & Interface bounds
<T extends ClassBound & InterfaceBound1 & InterfaceBound2>
// Example: T must be a subclass of Vehicle AND implement both Runnable and Serializable
<T extends Vehicle & Runnable & Serializable>
public void processVehicleTask(T task) {
// Inside this method, we can safely call methods from Vehicle, Runnable, and use it as Serializable
task.accelerate(1.0); // Method from Vehicle contract
new Thread(task).start(); // Use as Runnable
// serializeObject(task); // Use as Serializable (assuming method exists)
}
// Example: Bound only by interfaces
<S extends Readable & java.io.Closeable>
public void readAndClose(S source) throws java.io.IOException {
// Can safely call methods from both Readable and Closeable interfaces
// ... read data using source.read(...) ... (method from Readable)
source.close(); // method from Closeable
}
Why use multiple bounds? They allow you to create generic types or methods that operate on objects needing a specific combination of structural inheritance and specific capabilities. For example, our processVehicleTask needs something that is-a Vehicle but also can-do the actions defined by Runnable and Serializable. Multiple bounds provide precise compile-time guarantees for these complex requirements.
Bounds allow us to write more powerful and specific generic algorithms and data structures, such as methods for numeric calculations, comparisons, sorting, or interacting with types having specific capabilities. However, bounds constrain the types that can be used. Sometimes we need the opposite: more flexibility in accepting related generic types as parameters, even if we don’t know the exact type argument. For this, we use wildcards, which we explore next.
To allow the generic code to work with primitive types like int.
Incorrect. Bounds restrict generic parameters to reference types that meet certain criteria; they don’t enable direct use of primitives.
To bypass type erasure at runtime.
Incorrect. Bounds influence compile-time checks and how erasure occurs (replacing T with the bound instead of Object), but they don’t prevent erasure.
To allow the generic type to be instantiated using new T().
Incorrect. The inability to use new T() is a limitation due to type erasure, which bounds don’t solve directly (though workarounds exist).
To guarantee that the type parameter T has access to specific methods beyond those defined in Object (e.g., doubleValue() or compareTo()).
Correct. Bounds provide a contract to the compiler that objects of type T will have the methods defined by the bound (e.g., Number or Comparable), allowing the generic code to safely call them.
Incorrect. If a class bound (Thread) is present, it must be listed first.
<T extends Number & Integer>
Incorrect. A type parameter can extend at most one class (Number or Integer, not both).
<T extends Number & Comparable<T> & Serializable>
Correct. This follows the rule: at most one class bound (Number) listed first, followed by interface bounds (Comparable, Serializable) separated by ampersands.
<T extends Comparable<T> , Serializable>
Incorrect. Multiple bounds must be separated by an ampersand (&), not a comma.