Wildcards in Generics in Java – Complete Guide

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

  1. Unbounded Wildcards (?): Most flexible, but you can only read as Object
  2. Upper Bounded Wildcards (? extends T): Read-only, accepts T and its subtypes
  3. Lower Bounded Wildcards (? super T): Write-mostly, accepts T and its supertypes
  4. PECS Principle: Producer-Extends, Consumer-Super
  5. Use wildcards for maximum API flexibility
  6. Avoid wildcards in return types when possible
  7. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper