How to use wildcards for type parameters in Java

Introduction

We can use wildcard arguments with type parameters in Java to represent unknown types. A wildcard is a symbol that can represent any type. This is useful when we don't know the type of an object in advance or when we want to allow for multiple types. For example, consider the following code, which declares a list of strings:

List<String> list = new ArrayList<>();
A list to store elements of concrete type

In the code above, the list variable can only store String objects. In this answer, we will learn about the following wildcard arguments:

  • ?: This is used for unknown types. It can further be bounded using these arguments:
    • Upper bound: ? super
    • Lower bound: ? extends
  • &: This is used for combining multiple constraints.

Specifying wildcard with type parameters

We can use a wildcard argument as follows to make this code flexible:

List<?> list = new ArrayList<>();
A list to store elements of any type

In the code above, the list variable can store objects of any type.

Code example

Java also allows us to use wildcard arguments in type parameters. For example, the following code defines a generic class that takes a list of any type:

import java.util.*;
class WildcardExample {
public static void printList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
public static void main( String args[] ) {
List<String> list1 = new ArrayList<>();
list1.add("Hello");
list1.add("World");
List<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
printList(list1);
printList(list2);
}
}

Explanation

  • Line 5: We define the printList() method that takes a list of any type. We can use this method to print out the elements in the list.
  • Lines 13–15: We create a list of strings named list1 and add two strings to the list.
  • Lines 17–19: We create a list of integers named list2 and add two integers to the list.
  • Line 21: We call the printList() method with a list of strings to print out each string in the list, as follows:
Hello
World
  • Line 22: We call the printList() method with a list of integers to print out each integer element in the list, as follows:
1
2

Specifying upper bound type constraints

We can also specify an upper bound for the wildcard argument, as in the following:

List<? super Number> list = new ArrayList<>();

In the code snippet above, the list variable can only store objects of type number or it's supertypes.

Code example

import java.util.*;
class WildcardExampleSuper {
public static void printList(List<? super Integer> list) {
for (Object o : list) {
System.out.println(o);
}
}
public static void main( String args[] ) {
List<Object> list1 = new ArrayList<>();
list1.add("Hello");
list1.add(3);
printList(list1);
}
}

Explanation

  • Lines 13–15: We create a list named list1 of type Object and add two objects to the list.
  • Line 15: We call the printList() method with a list of objects to print out each element in the list.

Specifying lower bound type constraint

We can also specify a lower bound for the wildcard argument, as in the following:

List<? extends Number> list = new ArrayList<>();
A list to store objects of type Number or its subtypes

In the above code snippet, the list variable can only store objects of type Number or its subtypes.

Code example

In the following code, we define a generic class that takes a list of a type that extends a Number class:

import java.util.*;
class WildcardExample {
public static void printList(List<? extends Number> list) {
for (Object o : list) {
System.out.println(o);
}
}
public static void main( String args[] ) {
List<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
printList(list2);
}
}

Explanation

  • Line 5: We define the printList() method that takes a list of a type that extends a Number class. We can use this method to print out the elements in the list.
  • Lines 13–15: We create a list of integers named list2 and add two integers to the list.
  • Line 17: We call the printList() method with a list of integers to print out each integer element in the list, as follows:
1
2

Specifying multiple constraint types

We can specify multiple types of constraints by using the & operator. For example, the following code defines a generic class that takes a list of objects that are both Comparable and Serializable:

List<T extends Comparable & Serializable> list = new ArrayList<>();

Code example

The following code defines a generic class with multiple type constraints:

import java.io.Serializable;
import java.util.*;
class Shape implements Serializable, Comparable{
String name;
Shape(String name) {
this.name = name;
}
@Override
public int compareTo(Object o) {
return 0;
}
@Override
public String toString() {
return "Shape {name = " + this.name + "}";
}
}
class MultipleTypeConstraints <T extends Comparable & Serializable> {
public void printList(List<T> list) {
for (T t: list) {
System.out.println(t);
}
}
public static void main(String[] args) {
MultipleTypeConstraints<Shape> instance = new MultipleTypeConstraints<>();
List<Shape> list = new ArrayList<>();
Shape circle = new Shape("circle");
Shape square = new Shape("square");
list.add(circle);
list.add(square);
instance.printList(list);
}
}

Explanation

  • Line 4: We create a class Shape that implements both Serializable and Comparable interfaces.
  • Line 13: We override the compareTo() method of the Comparable interface.
  • Line 16: We override the toString() method to print a user-friendly message.
  • Line 23: We create a class MultipleTypeConstraints that takes a type parameter T with multiple type constraints. The type parameter T should extend (or implement) both Comparable and Serializable interfaces.
  • Line 24: We create a method printList() that takes a list of objects of type T and prints them.
  • Line 31: We create an object of the class MultipleTypeConstraints.
  • Line 32: We create a list of Shape objects.
  • Line 33: We create an instance of Shape class with object name as circle. We passed "circle" as the name parameter to its constructor.
  • Line 34: We create an instance of Shape class with object name as square. We passed "square" as the name parameter of its constructor.
  • Lines 35–36: We add the circle and the square objects to the list.
  • Line 38: We call the method on the instance object and pass the list as a parameter. When we run the above program, we get the following output:
Shape {name = circle}
Shape {name = square}
Output of the program

Conclusion

In this Answer, we learned about wildcards arguments in Java and how to use them. We also saw how to specify multiple type constraints by using the & operator.

Free Resources