Generics are a fundamental part of modern Java, introduced in Java 5 to add compile-time type safety to collections and other container classes. Before generics, Java collections stored objects as raw types, meaning everything was treated as Object
. This often required explicit casting and increased the risk of runtime errors. Generics solve this by allowing developers to specify the type of objects stored in a collection, leading to safer and cleaner code.
Generics offer several important benefits:
Declaring a collection with generics is straightforward:
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
String firstName = names.get(0); // No casting needed
In this example, List<String>
declares that names
is a list of String
objects. The compiler ensures only String
elements can be added.
Without generics, the same code might look like:
List names = new ArrayList();
names.add("Alice");
names.add("Bob");
String firstName = (String) names.get(0); // Explicit cast
This version is less safe, as you might mistakenly add a non-string object and only find out at runtime.
Generics work with all collection types:
Set<Integer> numbers = new HashSet<>();
Map<String, Integer> scores = new HashMap<>();
Queue<Double> prices = new LinkedList<>();
Here, the generic type ensures only the correct data types are added.
Since Java 7, you can use the diamond operator <>
to let the compiler infer the type:
List<String> cities = new ArrayList<>(); // Type inferred as ArrayList<String>
This makes declarations cleaner while maintaining type safety.
The compiler will flag incorrect types:
List<Integer> ids = new ArrayList<>();
ids.add(10);
ids.add("abc"); // Compile-time error: incompatible types
Such early feedback prevents many bugs that would otherwise occur at runtime.
Generics are essential for writing robust, maintainable Java code with collections. By specifying type parameters, you get strong compile-time checks, eliminate the need for casting, and produce cleaner, more self-explanatory code. Understanding and using generics effectively will greatly enhance your experience working with Java’s Collections Framework.
Java generics offer powerful flexibility, especially when working with collections, but there are situations where specifying exact types becomes too restrictive. This is where wildcards (?
) and bounded types (extends
, super
) come in. Wildcards help generalize method parameters and return types, improving reusability and allowing safe polymorphic behavior with generics.
Consider this basic generic method:
public void printList(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
This method seems generic, but surprisingly, it won’t accept a List<String>
or List<Integer>
due to type invariance in Java generics. Even though String
is a subtype of Object
, List<String>
is not a subtype of List<Object>
. To solve this, we use a wildcard:
public void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
Here, ?
represents an unknown type. Now, printList
can accept List<String>
, List<Integer>
, or any other List<T>
. However, you can only read from such lists—not add new elements (except null
)—because the exact type is unknown.
Bounded wildcards restrict the range of allowable types and are commonly used to increase flexibility while preserving type safety.
? extends T
(Upper Bound)Use ? extends T
when you want to read from a collection but don’t need to write to it:
public double sum(List<? extends Number> numbers) {
double total = 0;
for (Number num : numbers) {
total += num.doubleValue();
}
return total;
}
This method can accept List<Integer>
, List<Double>
, etc., because all extend Number
. But adding to numbers
is disallowed, as the actual subtype is unknown.
? super T
(Lower Bound)Use ? super T
when you want to write to a collection:
public void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
This method accepts List<Integer>
, List<Number>
, or List<Object>
—any list that can safely store Integer
values.
These terms describe how type relationships behave under inheritance:
? extends T
) allows a method to accept subtypes of T
. It’s read-only (safe to get, not to put).? super T
) allows a method to accept supertypes of T
. It’s write-safe (you can add T
, but retrieving yields Object
).Java's wildcard system enforces these constraints to avoid runtime type errors.
// Covariant read
List<? extends Number> nums = new ArrayList<Integer>();
Number n = nums.get(0); // okay
// nums.add(3); // compile error
// Contravariant write
List<? super Integer> objects = new ArrayList<Number>();
objects.add(42); // okay
// Integer i = objects.get(0); // compile error – returns Object
Wildcards and bounded types are essential tools for writing flexible, reusable, and type-safe generic code in Java. Use ? extends
when you only need to read from a collection, and ? super
when you need to write. These constructs, together with careful method design, help you safely navigate complex generic scenarios while preserving robust compile-time type checking.
Generic methods let us write flexible, reusable code that works with different types of collections without sacrificing type safety. In this section, we’ll walk through several runnable examples showing how to define and use generic methods with collections, wildcards, and bounded types.
Let’s write a method that copies elements from one collection to another. This method will use wildcards and generics to ensure type safety.
import java.util.*;
public class GenericCopyExample {
// Copy from a source Collection of type T or its subtype into a destination Collection of T or its supertype
public static <T> void copyElements(Collection<? extends T> source, Collection<? super T> destination) {
for (T item : source) {
destination.add(item);
}
}
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Number> numbers = new ArrayList<>();
// Copy integers into a list of Numbers
copyElements(integers, numbers);
System.out.println("Numbers: " + numbers); // Output: Numbers: [1, 2, 3]
}
}
Explanation:
<? extends T>
allows reading items of type T (or subtype).<? super T>
allows writing items of type T (or supertype).Now we’ll implement a generic method that returns the maximum element from a collection, using bounded types to ensure the elements are comparable.
import java.util.*;
public class MaxFinder {
// T must be a subtype of Comparable<T> to ensure elements can be compared
public static <T extends Comparable<T>> T findMax(Collection<T> collection) {
if (collection.isEmpty()) {
throw new IllegalArgumentException("Collection is empty");
}
Iterator<T> iterator = collection.iterator();
T max = iterator.next();
while (iterator.hasNext()) {
T current = iterator.next();
if (current.compareTo(max) > 0) {
max = current;
}
}
return max;
}
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "David", "Bob");
String maxName = findMax(names);
System.out.println("Max (lexicographically): " + maxName); // Output: Max (lexicographically): David
List<Integer> numbers = Arrays.asList(10, 20, 5);
int maxNum = findMax(numbers);
System.out.println("Max number: " + maxNum); // Output: Max number: 20
}
}
Explanation:
<T extends Comparable<T>>
to ensure that type T
can be compared with itself.Integer
, String
).These examples demonstrate:
? extends
, ? super
) for flexibility.<T extends Comparable<T>>
) to enforce constraints on operations like comparison.Using generics in this way helps you write more reusable, type-safe utilities for collections in Java.