One of the most common pitfalls when learning Java Generics is understanding how type inheritance works with generic parameters. In Java, generic collections are invariant.
Even though a Student is a subclass of Person, a list of students (List<Student>) is not a subclass of a list of people (List<Person>). If Java allowed this inheritance, you could pass a List<Student> to a method accepting List<Person>, and that method could insert a Teacher into the list. This would corrupt the list's types, triggering a ClassCastException later. To enable safe polymorphic relationships in collections, Java introduced wildcards (?) and bounded types (extends and super). In this guide, we will demystify wildcards using a simple school campus hierarchy.
To visualize these bounds, imagine a school campus populated by three entities: People, Students, and Teachers. Both Students and Teachers are types of People.
- Upper Bounded Wildcard (
? extends Person): A security sensor at the library gate says: "Only People and their subclasses (Students and Teachers) are allowed inside." An unrelated entity like a dog or a vehicle cannot enter. We are reading their credentials as they enter, but we cannot add new people to the library without verifying their specific role. - Lower Bounded Wildcard (
? super Teacher): A committee rule for class evaluation says: "Only Teachers and their superclasses (all People) are allowed to grade exams." Students are strictly excluded. - Unbounded Wildcard (
?): A clicker counter at the main gate says: "I don't care who you are—Student, Teacher, or random visitor—I just want to count how many entities pass through."
The Class Hierarchy Setup
For our examples, we will use this basic inheritance tree: Student and Teacher extend Person. There is also an unrelated class named Sardar:
class Person { String name; }
class Student extends Person {}
class Teacher extends Person {}
class Sardar {} // Unrelated class
1. Upper Bounded Wildcards (? extends T)
An upper bound restricts a generic parameter to class T or any of its subclasses. This allows you to safely read from the collection, as every item is guaranteed to be at least a Person. However, you cannot write elements to this list (except null) because the compiler cannot guarantee the specific subclass of the list at runtime.
static int count = 0;
static void countitem(ArrayList<? extends Person> l) {
// Only accept lists of Person, Student, or Teacher
for (Object o : l) {
count++;
}
System.out.println("Items added : " + count);
count = 0;
}
If we try to invoke this method with ArrayList<Sardar>, the compilation fails because Sardar is not a subclass of Person.
2. Lower Bounded Wildcards (? super T)
A lower bound restricts a generic parameter to class T or any of its superclasses (climbing up the inheritance chain). This allows you to safely write elements of type T (or its subclasses) to the collection. However, you can only safely read objects as the base Object type, because the list could be a List<Object> or List<Person>.
static void countitem(ArrayList<? super Teacher> l) {
// Accept lists of Teacher, Person, or Object
for (Object o : l) {
count++;
}
}
public static void main(String args[]) {
ArrayList<Person> ap = new ArrayList<>();
ArrayList<Student> as = new ArrayList<>();
ArrayList<Teacher> at = new ArrayList<>();
countitem(ap); // Works! Person is superclass of Teacher
countitem(at); // Works! Teacher matches Teacher
// countitem(as); // Fails! Student is not superclass of Teacher
}
3. Unbounded Wildcards (?)
If you do not care about the type parameter and want to perform operations that only depend on the collection structure (like checking size), use the raw wildcard ?:
static void addnum(ArrayList<?> l) { // Unbounded wildcard
System.out.println("List size: " + l.size());
}
This method accepts an ArrayList containing any object type (String, Integer, Student, etc.) without throwing errors.
Conclusion & The PECS Rule
When deciding which wildcard to use, remember the PECS guideline: Producer Extends, Consumer Super.
- Producer Extends: Use
? extends Tif you only read from the collection. - Consumer Super: Use
? super Tif you only write to the collection.