Introduction to Bounded Type Parameters in Java
Java generics provide a powerful mechanism for writing type-safe code while maintaining flexibility. The concept of bounded type parameters adds an additional layer of specificity, allowing developers to define constraints on the types that can be used in generics. By using bounded type parameters, you can limit the kinds of types that can be used with a generic class or method.
Before diving into bounded type parameters, it is important to understand the basic idea of Java Generics.
Basic Overview of Generics
Generics were introduced in Java 5 to allow for stronger type checks at compile time and to support generic algorithms. With generics, you can write a single class or method that works with different data types. For instance:
// Generic class that can hold any type of object
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
Here, the class Box
is generic, meaning it can be used with any data type. The type parameter T
will be replaced with a concrete type when an instance of Box
is created.
What Are Bounded Type Parameters?
A bounded type parameter allows you to impose a restriction on the types that can be used as arguments for a generic class, interface, or method. This ensures that the generic type is constrained to be a subtype (or a specific type) of a particular class or interface.
In simpler terms, bounded type parameters let you define that the type parameter should extend a particular class or interface, providing more flexibility while also maintaining type safety.
There are two types of bounds in Java generics:
- Upper Bounded Type Parameters: This restricts the type to be a subclass of a specific class or interface (i.e., the type is “less than or equal to” the bound type).
- Lower Bounded Type Parameters: This restricts the type to be a superclass of a specific class or interface (i.e., the type is “greater than or equal to” the bound type).
1. Upper Bounded Type Parameters
In Java, you can use an upper bound by using the extends
keyword. This ensures that the type parameter is either a specific class or a subclass of that class.
Syntax for Upper Bounded Type Parameters:
class ClassName<T extends BaseClass> {
// The type parameter T must be of type BaseClass or a subclass of BaseClass
}
Example of Upper Bounded Type Parameters
Let’s consider a method that works with numbers and computes the sum of a collection of values. We can define an upper-bounded type parameter to ensure that the type is a subclass of Number
(like Integer
, Double
, etc.).
import java.util.List;
class SumCalculator {
// This method is constrained to work with types that are subclasses of Number
public static <T extends Number> double sum(List<T> numbers) {
double sum = 0;
for (T number : numbers) {
sum += number.doubleValue(); // Using doubleValue() of Number class
}
return sum;
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4);
List<Double> doubleList = List.of(1.5, 2.5, 3.5);
System.out.println("Sum of Integers: " + sum(intList)); // 10.0
System.out.println("Sum of Doubles: " + sum(doubleList)); // 7.5
}
}
In this example, the sum
method works for any type that is a subclass of Number
, such as Integer
, Double
, or Float
. The type parameter T
is bounded by Number
, ensuring that only numerical types are allowed.
Why Use Upper Bounded Type Parameters?
- Code Reusability: You can write a method or class that works with different subclasses of a base class.
- Type Safety: By bounding the type, you ensure that only compatible types are used.
- Flexibility: You can still work with a variety of subclasses, allowing for a broad range of use cases.
2. Lower Bounded Type Parameters
A lower bounded type parameter allows a type argument that is a superclass of a particular class. This is useful in cases where you want to work with a wider range of types.
Syntax for Lower Bounded Type Parameters:
class ClassName<T super BaseClass> {
// The type parameter T must be of type BaseClass or a superclass of BaseClass
}
Example of Lower Bounded Type Parameters
Let’s take an example where you want to add elements to a collection. We can use a lower bounded type parameter to ensure that the collection only accepts Number
types or any superclass of Number
.
import java.util.List;
class ListPrinter {
// This method accepts a list of any class that is a superclass of Number
public static <T> void printNumbers(List<? super T> numbers) {
for (Object number : numbers) {
System.out.println(number);
}
}
public static void main(String[] args) {
List<Number> numberList = List.of(1, 2.5, 3.8);
List<Object> objectList = List.of(100, "Hello", 200.5);
printNumbers(numberList); // Prints numbers: 1, 2.5, 3.8
printNumbers(objectList); // Prints object: 100, Hello, 200.5
}
}
In this case, the printNumbers
method accepts a list of any type that is a superclass of T
. For example, it can accept a list of Number
or Object
. This flexibility allows the method to work with broader ranges of types.
Why Use Lower Bounded Type Parameters?
- Allowing Subtypes: You can add elements of a certain class type or any of its subtypes into a collection.
- Broad Compatibility: It allows methods to accept more general types that are still compatible with the expected class type.
3. Wildcards with Bounded Type Parameters
Java also provides wildcards (?
) with generics. Wildcards allow you to specify an unknown type and apply bounds to it.
Upper-Bounded Wildcards:
You can use ? extends T
to specify an upper bound for an unknown type. For example, a method that can work with any subclass of Number
:
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
Lower-Bounded Wildcards:
For lower bounds, you can use ? super T
to specify that the type is a superclass of T
.
public static void addNumbers(List<? super Integer> list) {
list.add(10); // You can add Integer or any superclass type (e.g., Number)
}
Conclusion
Bounded type parameters in Java generics provide powerful ways to control the types that are passed into generic classes and methods. By defining upper and lower bounds, developers can create more flexible, type-safe, and reusable code. Whether you’re working with numerical types, objects, or implementing custom constraints, bounded type parameters ensure that your code is both robust and adaptable to a wide range of use cases.