Introduction
When working with Java generics, one of the key concepts to understand is the use of wildcards. Wildcards in Java offer flexibility when dealing with parameterized types. However, when you want more control over the types, you can use bounded wildcards. This feature allows you to specify an upper or lower bound for the type parameter, which makes your code more adaptable and type-safe.
In this article, we’ll explore how to use bounded wildcards in Java methods, explaining both upper-bounded and lower-bounded wildcards, and when each is appropriate to use. Along with theoretical explanations, we will also dive into practical code examples that illustrate how bounded wildcards work in Java.
What are Wildcards in Java?
Before diving into bounded wildcards, let’s first review what wildcards are in Java.
A wildcard is a special type parameter that allows you to write more generic code that works with different types. In Java, the wildcard is represented by a ?
. For example:
List<?> list;
This means the list can hold objects of any type, but you cannot directly add elements to this list, except for null
.
Types of Bounded Wildcards
There are two types of bounded wildcards in Java:
- Upper-bounded wildcards (
? extends T
):- The wildcard is restricted to a type that is either
T
or a subtype ofT
. - Use
? extends T
when you want to read from a structure, but you do not intend to write into it.
- The wildcard is restricted to a type that is either
- Lower-bounded wildcards (
? super T
):- The wildcard is restricted to a type that is either
T
or a supertype ofT
. - Use
? super T
when you want to write into a structure but do not care about the type you’re reading.
- The wildcard is restricted to a type that is either
Understanding Upper-Bounded Wildcards (? extends T
)
An upper-bounded wildcard means that the generic type can be of type T
or any class that extends T
. It’s useful when you want to restrict the types that can be passed into a method but still retain the ability to handle a broader range of types. The upper-bounded wildcard is defined as ? extends T
.
Example of Upper-Bounded Wildcards
Here’s an example of an upper-bounded wildcard being used in a Java method:
public class WildcardExample {
public static void printNumbers(List<? extends Number> numbers) {
for (Number num : numbers) {
System.out.println(num);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
printNumbers(intList); // Works with Integer
printNumbers(doubleList); // Works with Double
}
}
Explanation:
List<? extends Number>
means the list can hold any subclass ofNumber
, includingInteger
,Double
,Float
, etc.- The method
printNumbers
can accept lists of different numeric types without specifying an exact type, offering flexibility.
When to Use Upper-Bounded Wildcards
- When you only need to read data: Since you can’t add to a
List<? extends T>
, this wildcard is ideal for methods that don’t modify the collection. - When you want to accept multiple subtypes: For example, if you need a method that works with both
Integer
andDouble
, the upper-bounded wildcard allows you to accept both.
Understanding Lower-Bounded Wildcards (? super T
)
A lower-bounded wildcard means that the generic type can be of type T
or any class that is a superclass of T
. It’s useful when you want to write to a collection but don’t care about the specific type you’re working with. The lower-bounded wildcard is defined as ? super T
.
Example of Lower-Bounded Wildcards
public class WildcardExample {
public static void addIntegers(List<? super Integer> list) {
list.add(10);
list.add(20);
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addIntegers(numberList); // Works because Number is a supertype of Integer
System.out.println(numberList);
}
}
Explanation:
List<? super Integer>
means the list can holdInteger
objects or any type that is a superclass ofInteger
, likeNumber
orObject
.- The method
addIntegers
can safely addInteger
objects to the list because we know the list will acceptInteger
and possibly superclasses ofInteger
.
When to Use Lower-Bounded Wildcards
- When you need to write data: The lower-bounded wildcard allows you to add objects of type
T
or its subclasses. - When you know the list can accept values of type
T
: Since? super T
allows writing to the list, it’s often used when you want to add elements of typeT
or a subclass.
Combining Bounded Wildcards with Generics
You can combine both upper and lower bounds with generics for more complex scenarios. The syntax can be extended to accept parameters that are bounded by both upper and lower bounds.
Example of Upper and Lower Bounded Wildcards Together
public class WildcardExample {
public static <T> void printInRange(List<? extends T> list, T lowerBound, T upperBound) {
for (T element : list) {
if (((Comparable<T>) element).compareTo(lowerBound) >= 0 &&
((Comparable<T>) element).compareTo(upperBound) <= 0) {
System.out.println(element);
}
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5);
printInRange(intList, 2, 4); // Prints 2, 3, 4
}
}
Explanation:
- The method
printInRange
uses an upper-bounded wildcard (? extends T
) to accept a list of elements that extendT
(in this case,Integer
). - We are also using the
Comparable
interface to compare the elements within the range, demonstrating how both upper and lower bounds can be used together in a method.
Limitations of Bounded Wildcards
While bounded wildcards provide a lot of flexibility, there are some limitations you should be aware of:
- Reading from collections: With
? extends T
, you can only read elements but cannot add elements (except fornull
), as the compiler cannot guarantee the exact type of the elements in the collection. - Writing to collections: With
? super T
, you can only safely add objects of typeT
or its subtypes, but reading from the collection can be problematic since the exact type is unknown.
Common Use Cases
- Working with Collections: Bounded wildcards are useful when you want to create methods that can operate on collections of different types, such as lists of numbers, or lists of any subclass of a particular class.
- Polymorphism: Wildcards allow you to write methods that are polymorphic in terms of the types they accept, making your code cleaner and more reusable.
Conclusion
In Java, bounded wildcards provide a powerful way to work with generics while still enforcing type safety. By using upper-bounded wildcards (? extends T
) and lower-bounded wildcards (? super T
), you can create methods that are flexible and adaptable to a wide variety of types. Understanding when and how to use these wildcards will help you write more efficient and reusable code.
Key Points Recap:
- Upper-bounded wildcards (
? extends T
) are useful when reading data from a collection. - Lower-bounded wildcards (
? super T
) are useful when writing data to a collection. - Bounded wildcards increase flexibility and help avoid unnecessary type casting.
By mastering bounded wildcards, you will improve your Java generics knowledge and be able to create methods that handle a variety of types with type safety in mind.