In Java, generics provide a way to write flexible, reusable, and type-safe code. However, there are situations where we may not know or care about the specific type of a class but still want to work with a certain type of class. This is where wildcards in generics come into play. Wildcards allow for more flexible code that can work with a range of types.
What Are Wildcards in Java Generics?
A wildcard in Java is a special symbol represented by the question mark (?
) that can represent any type. It is used in generic types to specify that a parameter can accept any type of object, offering a higher level of flexibility. Wildcards are particularly useful when writing methods that work with generic types, and you don’t care about the exact type but just need to ensure that certain conditions are met.
Why Use Wildcards?
Wildcards offer flexibility when working with generics. Consider the following scenarios:
- When you want to pass a generic object but don’t care about the type.
- When you need to ensure that the objects passed are of a certain type or subtype, but the specific class doesn’t matter.
- When you want to improve the code’s reusability while still maintaining type safety.
Types of Wildcards in Java Generics
In Java, wildcards can be classified into three types based on their usage and constraints:
1. Unbounded Wildcard (?
)
The simplest wildcard is the unbounded wildcard, represented by ?
. It can represent any type of object, and it is typically used when you want to accept any kind of object in a generic context without any specific constraint. However, this means you lose the ability to perform operations that require knowledge about the type of the object.
List> list = new ArrayList<>();
list.add(new String("Hello"));
list.add(new Integer(10));
Note: You can’t add elements to a List>
because the type of elements is unknown.
2. Upper-Bounded Wildcard (? extends T
)
The upper-bounded wildcard allows you to specify that the generic type must be a subtype of a particular type T
. This is useful when you are working with a class hierarchy and want to restrict the types to subclasses of a given class.
public static void printList(List extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
List intList = Arrays.asList(1, 2, 3);
printList(intList);
Note: With the upper-bounded wildcard, you can read items from the list (since they are guaranteed to be of type Number
or its subclasses) but cannot add items to the list.
3. Lower-Bounded Wildcard (? super T
)
The lower-bounded wildcard is the opposite of the upper-bounded wildcard. It specifies that the type must be a superclass of a particular type T
. This is typically used when you want to allow for any type that is a superclass of T
so that you can add instances of T
or its subclasses to a collection.
public static void addNumbers(List super Integer> list) {
list.add(10);
list.add(20);
}
List numList = new ArrayList<>();
addNumbers(numList);
Note: With the lower-bounded wildcard, you can add elements of type Integer
or its subclasses but you can’t safely retrieve elements from the list.
Practical Examples of Wildcards in Java
Example 1: Using Unbounded Wildcard
The following example shows how the unbounded wildcard can be used when you don’t need to know the exact type of elements in the collection.
public static void printList(List> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
List stringList = Arrays.asList("Hello", "World");
printList(stringList);
List intList = Arrays.asList(1, 2, 3);
printList(intList);
In this example, the printList
method accepts a list of any type, and it works for both a List
and a List
without issue.
Example 2: Using Upper-Bounded Wildcard
The following code demonstrates the use of an upper-bounded wildcard. We use it to print all types that are subclasses of Number
:
public static void printNumbers(List extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
List doubleList = Arrays.asList(1.1, 2.2, 3.3);
printNumbers(doubleList);
Example 3: Using Lower-Bounded Wildcard
Now, let’s see how the lower-bounded wildcard can be used when we need to add elements to a collection.
public static void addToList(List super Integer> list) {
list.add(100);
list.add(200);
}
List numberList = new ArrayList<>();
addToList(numberList);
Here, addToList
accepts a list of any class that is a superclass of Integer
, such as Number
or Object
. This allows us to add Integer
values to the list.
Key Benefits of Using Wildcards in Java
- Flexibility: Wildcards allow you to write methods and classes that can handle a wide range of types, without the need for creating specific methods for every possible type.
- Type Safety: Wildcards preserve type safety, ensuring that the correct types are used when working with collections.
- Code Reusability: By using wildcards, you can write code that can be reused with different types, reducing redundancy and improving maintainability.
Conclusion
Wildcards in Java generics are a powerful tool for improving the flexibility and reusability of your code. Whether you’re working with unbounded, upper-bounded, or lower-bounded wildcards, understanding how they work can greatly enhance your ability to write clean, efficient, and type-safe code.