Overview
Wildcards in Java Generics provide flexibility when working with parameterized types. They allow you to write methods that can operate on collections of different types while maintaining type safety.
Types of Wildcards
1. Unbounded Wildcards (?)
Basic Syntax
import java.util.*;
public class UnboundedWildcard {
// Method that accepts any type of List
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
// Method that accepts any type of Collection
public static int getSize(Collection<?> collection) {
return collection.size();
}
public static void main(String[] args) {
List<String> stringList = Arrays.asList("A", "B", "C");
List<Integer> integerList = Arrays.asList(1, 2, 3, 4);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
printList(stringList); // A B C
printList(integerList); // 1 2 3 4
printList(doubleList); // 1.1 2.2 3.3
System.out.println("String list size: " + getSize(stringList));
System.out.println("Integer list size: " + getSize(integerList));
}
}
Practical Examples
import java.util.*;
public class UnboundedWildcardPractical {
// Check if collection contains null
public static boolean containsNull(Collection<?> collection) {
for (Object element : collection) {
if (element == null) {
return true;
}
}
return false;
}
// Convert any list to string representation
public static String listToString(List<?> list) {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (Object element : list) {
sb.append(element).append(", ");
}
if (!list.isEmpty()) {
sb.setLength(sb.length() - 2); // Remove trailing comma and space
}
sb.append("]");
return sb.toString();
}
// Count elements that satisfy a condition
public static long countElements(Collection<?> collection) {
return collection.stream().count();
}
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", null, "Bob");
List<Integer> numbers = Arrays.asList(1, 2, 3);
Set<Double> prices = new HashSet<>(Arrays.asList(10.5, 20.3, 30.7));
System.out.println("Names contains null: " + containsNull(names));
System.out.println("Numbers contains null: " + containsNull(numbers));
System.out.println("Names: " + listToString(names));
System.out.println("Numbers: " + listToString(numbers));
System.out.println("Prices count: " + countElements(prices));
}
}
2. Upper Bounded Wildcards (? extends Type)
Basic Syntax
import java.util.*;
public class UpperBoundedWildcard {
// Method that works with List of any type that extends Number
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number number : list) {
sum += number.doubleValue();
}
return sum;
}
// Method that works with any type that extends Comparable
public static <T> T findMax(List<? extends Comparable<T>> list) {
if (list.isEmpty()) {
return null;
}
Comparable<T> max = list.get(0);
for (Comparable<T> element : list) {
if (element.compareTo((T) element) > 0) {
max = element;
}
}
return (T) max;
}
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
List<Float> floats = Arrays.asList(1.5f, 2.5f, 3.5f);
System.out.println("Sum of integers: " + sumOfList(integers));
System.out.println("Sum of doubles: " + sumOfList(doubles));
System.out.println("Sum of floats: " + sumOfList(floats));
// This won't compile - String doesn't extend Number
// List<String> strings = Arrays.asList("A", "B", "C");
// System.out.println(sumOfList(strings)); // Compilation error
}
}
Advanced Upper Bounded Examples
import java.util.*;
class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating");
}
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
public void bark() {
System.out.println(name + " is barking");
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
public void meow() {
System.out.println(name + " is meowing");
}
}
public class UpperBoundedAdvanced {
// Process a list of any type that extends Animal
public static void processAnimals(List<? extends Animal> animals) {
for (Animal animal : animals) {
animal.eat();
// We can only call Animal methods, not specific subclass methods
}
}
// Calculate average weight for any numeric list
public static double calculateAverage(List<? extends Number> numbers) {
if (numbers.isEmpty()) return 0.0;
double sum = 0.0;
for (Number number : numbers) {
sum += number.doubleValue();
}
return sum / numbers.size();
}
// Find maximum in a list of comparables
public static <T extends Comparable<T>> T findMaximum(List<? extends T> list) {
if (list.isEmpty()) return null;
T max = list.get(0);
for (T element : list) {
if (element.compareTo(max) > 0) {
max = element;
}
}
return max;
}
public static void main(String[] args) {
List<Dog> dogs = Arrays.asList(new Dog("Buddy"), new Dog("Max"));
List<Cat> cats = Arrays.asList(new Cat("Whiskers"), new Cat("Mittens"));
processAnimals(dogs);
processAnimals(cats);
List<Integer> ages = Arrays.asList(25, 30, 35, 28, 32);
List<Double> salaries = Arrays.asList(50000.0, 75000.0, 60000.0);
System.out.println("Average age: " + calculateAverage(ages));
System.out.println("Average salary: " + calculateAverage(salaries));
System.out.println("Max age: " + findMaximum(ages));
System.out.println("Max salary: " + findMaximum(salaries));
}
}
3. Lower Bounded Wildcards (? super Type)
Basic Syntax
import java.util.*;
public class LowerBoundedWildcard {
// Method that can add integers to any list that can hold integers
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}
// Method that can add animals to any list that can hold animals
public static void addAnimals(List<? super Animal> list, Animal animal) {
list.add(animal);
}
// Copy from source (producer) to destination (consumer)
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T element : src) {
dest.add(element);
}
}
public static void main(String[] args) {
// Lower bounded examples
List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
addNumbers(numberList); // OK - Number super Integer
addNumbers(objectList); // OK - Object super Integer
addNumbers(integerList); // OK - Integer super Integer
System.out.println("Number list: " + numberList);
System.out.println("Object list: " + objectList);
System.out.println("Integer list: " + integerList);
// This won't compile - Double is not super Integer
// List<Double> doubleList = new ArrayList<>();
// addNumbers(doubleList); // Compilation error
// Copy example
List<Integer> source = Arrays.asList(1, 2, 3);
List<Number> destination = new ArrayList<>();
copy(destination, source);
System.out.println("After copy: " + destination);
}
}
PECS Principle (Producer-Extends, Consumer-Super)
import java.util.*;
public class PECSPrinciple {
// Producer - uses extends (reading from collection)
public static double sumProducer(List<? extends Number> numbers) {
double sum = 0.0;
for (Number number : numbers) {
sum += number.doubleValue();
}
return sum;
}
// Consumer - uses super (writing to collection)
public static void addConsumer(List<? super Integer> list, Integer value) {
list.add(value);
}
// Both producer and consumer
public static <T> void copyPECS(List<? super T> dest, List<? extends T> src) {
for (T element : src) {
dest.add(element);
}
}
// Process and transform
public static <T> void processAndAdd(
List<? extends T> source,
List<? super T> destination,
java.util.function.Function<T, T> transformer) {
for (T element : source) {
destination.add(transformer.apply(element));
}
}
public static void main(String[] args) {
// Producer example
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("Sum integers: " + sumProducer(integers));
System.out.println("Sum doubles: " + sumProducer(doubles));
// Consumer example
List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
addConsumer(numberList, 10);
addConsumer(objectList, 20);
System.out.println("Number list: " + numberList);
System.out.println("Object list: " + objectList);
// PECS copy example
List<String> sourceStrings = Arrays.asList("A", "B", "C");
List<Object> destObjects = new ArrayList<>();
copyPECS(destObjects, sourceStrings);
System.out.println("Copied strings: " + destObjects);
// Process and add example
List<Integer> sourceNumbers = Arrays.asList(1, 2, 3);
List<Number> resultNumbers = new ArrayList<>();
processAndAdd(sourceNumbers, resultNumbers, x -> x * 2);
System.out.println("Processed numbers: " + resultNumbers);
}
}
Real-World Examples
1. Collection Utilities with Wildcards
import java.util.*;
public class CollectionUtils {
// Filter elements based on predicate
public static <T> List<T> filter(List<? extends T> list,
java.util.function.Predicate<? super T> predicate) {
List<T> result = new ArrayList<>();
for (T element : list) {
if (predicate.test(element)) {
result.add(element);
}
}
return result;
}
// Map elements to different type
public static <T, R> List<R> map(List<? extends T> list,
java.util.function.Function<? super T, ? extends R> mapper) {
List<R> result = new ArrayList<>();
for (T element : list) {
result.add(mapper.apply(element));
}
return result;
}
// Add all elements from source to destination
public static <T> void addAll(List<? super T> destination,
Collection<? extends T> source) {
destination.addAll(source);
}
// Find first element matching predicate
public static <T> T findFirst(List<? extends T> list,
java.util.function.Predicate<? super T> predicate) {
for (T element : list) {
if (predicate.test(element)) {
return element;
}
}
return null;
}
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Filter even numbers
List<Integer> evens = filter(numbers, n -> n % 2 == 0);
System.out.println("Even numbers: " + evens);
// Map to strings
List<String> numberStrings = map(numbers, Object::toString);
System.out.println("Number strings: " + numberStrings);
// Add to destination with super type
List<Number> numberList = new ArrayList<>();
addAll(numberList, numbers);
System.out.println("Number list: " + numberList);
// Find first element greater than 5
Integer firstLarge = findFirst(numbers, n -> n > 5);
System.out.println("First number > 5: " + firstLarge);
}
}
2. Generic Repository with Wildcards
import java.util.*;
interface Entity {
Long getId();
void setId(Long id);
}
class User implements Entity {
private Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public Long getId() { return id; }
@Override
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
}
class Product implements Entity {
private Long id;
private String name;
private double price;
public Product(Long id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
@Override
public Long getId() { return id; }
@Override
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public double getPrice() { return price; }
@Override
public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + "}";
}
}
public class GenericRepository<T extends Entity> {
private Map<Long, T> storage = new HashMap<>();
private Long nextId = 1L;
// Save entity (consumer - uses super)
public void save(T entity) {
if (entity.getId() == null) {
entity.setId(nextId++);
}
storage.put(entity.getId(), entity);
}
// Save all entities (consumer - uses super)
public void saveAll(Collection<? extends T> entities) {
for (T entity : entities) {
save(entity);
}
}
// Find by ID (producer - uses extends)
public T findById(Long id) {
return storage.get(id);
}
// Find all (producer - uses extends)
public List<T> findAll() {
return new ArrayList<>(storage.values());
}
// Find by predicate (producer - uses extends)
public List<T> findBy(java.util.function.Predicate<? super T> predicate) {
List<T> result = new ArrayList<>();
for (T entity : storage.values()) {
if (predicate.test(entity)) {
result.add(entity);
}
}
return result;
}
// Update entity (both producer and consumer)
public void update(Long id, java.util.function.UnaryOperator<T> updater) {
T entity = findById(id);
if (entity != null) {
storage.put(id, updater.apply(entity));
}
}
public static void main(String[] args) {
GenericRepository<User> userRepo = new GenericRepository<>();
GenericRepository<Product> productRepo = new GenericRepository<>();
// Save users
userRepo.saveAll(Arrays.asList(
new User(null, "Alice"),
new User(null, "Bob"),
new User(null, "Charlie")
));
// Save products
productRepo.saveAll(Arrays.asList(
new Product(null, "Laptop", 999.99),
new Product(null, "Mouse", 29.99),
new Product(null, "Keyboard", 79.99)
));
// Find all users
System.out.println("All users:");
userRepo.findAll().forEach(System.out::println);
// Find users by predicate
System.out.println("\nUsers with name containing 'A':");
userRepo.findBy(user -> user.getName().contains("A"))
.forEach(System.out::println);
// Find all products
System.out.println("\nAll products:");
productRepo.findAll().forEach(System.out::println);
// Update product
productRepo.update(1L, product -> {
return new Product(product.getId(), product.getName() + " (Updated)",
product.getPrice() * 0.9);
});
System.out.println("\nAfter update:");
productRepo.findAll().forEach(System.out::println);
}
}
Wildcards with Multiple Bounds
import java.util.*;
public class MultipleBoundsWildcard {
interface Named {
String getName();
}
interface Priced {
double getPrice();
}
class Product implements Named, Priced {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String getName() { return name; }
@Override
public double getPrice() { return price; }
@Override
public String toString() {
return name + " - $" + price;
}
}
// Method with multiple bounds using wildcards
public static <T extends Named & Priced> void processItems(List<? extends T> items) {
for (T item : items) {
System.out.println("Processing: " + item.getName() + " at price $" + item.getPrice());
}
}
// Method that works with collections of any type that implements both interfaces
public static void printCatalog(List<? extends Named & Priced> items) {
System.out.println("Catalog:");
for (Named item : items) {
System.out.println("- " + item.getName());
}
}
public static void main(String[] args) {
MultipleBoundsWildcard instance = new MultipleBoundsWildcard();
List<Product> products = Arrays.asList(
instance.new Product("Laptop", 999.99),
instance.new Product("Mouse", 29.99),
instance.new Product("Keyboard", 79.99)
);
processItems(products);
printCatalog(products);
}
}
Common Patterns and Best Practices
import java.util.*;
public class WildcardBestPractices {
// GOOD: Use wildcards for maximum flexibility
public static void processCollection(Collection<?> collection) {
// Read operations are safe
for (Object item : collection) {
System.out.println(item);
}
}
// GOOD: Use upper bounds for producers (objects you read from)
public static double calculateTotal(List<? extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// GOOD: Use lower bounds for consumers (objects you write to)
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// GOOD: PECS in method signatures
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
dest.addAll(src);
}
// AVOID: Unnecessary wildcards in return types
// BAD: public static List<?> getList() { ... }
// GOOD: public static <T> List<T> getList() { ... }
// AVOID: Wildcards in class definitions (use type parameters instead)
// BAD: class Processor<?> { ... }
// GOOD: class Processor<T> { ... }
// Use wildcards for APIs that need to work with unknown types
public static boolean contains(Collection<?> collection, Object element) {
return collection.contains(element);
}
public static void main(String[] args) {
// Example usage
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Number> numbers = new ArrayList<>();
processCollection(integers);
System.out.println("Total: " + calculateTotal(integers));
addNumbers(numbers);
copy(numbers, integers);
System.out.println("Numbers: " + numbers);
}
}
Limitations and Important Notes
import java.util.*;
public class WildcardLimitations {
// You cannot instantiate with wildcards
public static void instantiationLimitation() {
// These are ILLEGAL:
// List<?> list = new ArrayList<?>();
// List<? extends Number> numbers = new ArrayList<? extends Number>();
// These are LEGAL:
List<?> list = new ArrayList<>();
List<? extends Number> numbers = new ArrayList<Integer>();
}
// You cannot use wildcards in instance creation
public static void instanceCreationLimitation() {
// ILLEGAL:
// SomeClass<?> obj = new SomeClass<?>();
// LEGAL:
List<String> stringList = new ArrayList<>();
List<?> wildcardList = stringList; // OK - assignment
}
// Generic method with wildcard return type
public static <T> List<T> createList(T element) {
List<T> list = new ArrayList<>();
list.add(element);
return list;
}
// You can have wildcards in method parameters but be careful with return types
public static List<?> createWildcardList() {
return new ArrayList<String>(); // OK, but limited usefulness
}
public static void main(String[] args) {
// Demonstration of limitations
List<?> wildcardList = new ArrayList<String>();
// You cannot add to unbounded wildcard list (except null)
// wildcardList.add("Hello"); // Compilation error
wildcardList.add(null); // This is allowed
// But you can read from it
Object element = wildcardList.get(0); // OK
System.out.println("Limitations demonstrated");
}
}
Key Points Summary
- Unbounded Wildcards (
?): Most flexible, but you can only read asObject - Upper Bounded Wildcards (
? extends T): Read-only, acceptsTand its subtypes - Lower Bounded Wildcards (
? super T): Write-mostly, acceptsTand its supertypes - PECS Principle: Producer-Extends, Consumer-Super
- Use wildcards for maximum API flexibility
- Avoid wildcards in return types when possible
- Wildcards cannot be used in instance creation
Wildcards provide the necessary flexibility to write generic code that works with different types while maintaining type safety, making them essential for building robust and reusable Java libraries and applications.