Understanding Java Generics: Theory, Wildcards, and Practical Examples
This article provides a comprehensive guide to Java generics, covering compilation vs runtime, generic classes, interfaces, methods, wildcard types, bounded and unbounded usage, and practical code examples demonstrating how to design flexible, type‑safe collection utilities and apply generic constraints effectively.
Introduction
Java generics are a core language feature that enable type‑safe collections and APIs. Introduced in JDK 1.5, they exist only at compile time; at runtime the type information is erased to Object . Understanding how generics work and how to use them correctly is essential for writing robust Java code.
Compilation vs Runtime
During compilation the source file ( .java ) is transformed into bytecode ( .class ) which the JVM loads and executes. Generics are checked by the compiler and disappear after compilation (type erasure).
What Is a Generic?
A generic, also called a parameterized type, lets you define a class, interface, or method with a placeholder type (the "bowl") that the user later substitutes with a concrete type (the "contents"). This improves safety by allowing the compiler to verify that only compatible objects are stored in collections.
Generic Class Example
public class GenericClass
{
// member variable
private T t;
public void function(T t) {
// implementation
}
// not a generic method, just uses the class's type parameter
public T functionTwo(T t) {
return t;
}
}Generic Interface Example
public interface GenericInterface
{
T get();
void set(T t);
T delete(T t);
default T defaultFunction(T t) {
return t;
}
}Generic Method Example
public class GenericFunction {
public
void function(T t) { }
public
T functionTwo(T t) { return t; }
public
String functionThree(T t) { return ""; }
}Wildcards
Wildcards ( ?> ) allow a generic type to be flexible about the actual type argument. Three forms exist:
<?> : unbounded wildcard – any type.
<? extends T> : upper‑bounded wildcard – any subtype of T ; read‑only.
<? super T> : lower‑bounded wildcard – any supertype of T ; write‑only.
These are often summarized by the PECS principle (Producer‑extends, Consumer‑super).
Wildcard Usage Scenarios
When a method only needs to read from a collection, use an upper‑bounded wildcard; when it only writes to a collection, use a lower‑bounded wildcard. If the method both reads and writes, use a plain generic type without wildcards.
Unbounded Wildcard Example
public static
int size(Collection
list) {
return list.size();
}
public static int sizeTwo(Collection
list) {
return list.size();
}Both methods work, but sizeTwo expresses that the element type is irrelevant.
Upper‑Bounded Wildcard Example (read‑only)
public static
int beMixedSum(Set
s1, Set
s2) {
int i = 0;
for (T t : s1) {
if (s2.contains(t)) i++;
}
return i;
}
public static int beMixedSumTwo(Set
s1, Set
s2) {
int i = 0;
for (Object o : s1) {
if (s2.contains(o)) i++;
}
return i;
}Both compute the intersection size; the wildcard version makes the intent clearer.
Upper‑Bounded Wildcard in a Utility Class
public class CollectionUtils
{
// generic version – works only when the source and target have the exact same type
public List
listCopy(Collection
collection) {
List
newCollection = new ArrayList<>();
for (T t : collection) {
newCollection.add(t);
}
return newCollection;
}
// upper‑bounded version – can copy from a collection of any subtype of T
public List
listCopyTwo(Collection
collection) {
List
newCollection = new ArrayList<>();
for (T t : collection) {
newCollection.add(t);
}
return newCollection;
}
}Using ? extends T allows copying from List or List into a List target.
Lower‑Bounded Wildcard Example (write‑only)
public class CollectionUtils
{
// lower‑bounded target – can write any subtype of T into the target collection
public void copy(List
target, List
src) {
if (src.size() > target.size()) {
for (int i = 0; i < src.size(); i++) {
target.set(i, src.get(i));
}
}
}
}This method can copy a List into a List because the target accepts any supertype of Son .
Bounded Type Parameters
Sometimes you need to restrict a generic type to a specific hierarchy. Use a bounded type parameter like <T extends Grandpa> to allow only Grandpa and its subclasses.
public class GenericClass
{
public void test(T t) { /* ... */ }
}
public static void main(String[] args) {
GenericClass
g1 = new GenericClass<>();
g1.test(new Grandpa());
g1.test(new Father());
g1.test(new Son());
GenericClass
g2 = new GenericClass<>();
g2.test(new Father());
g2.test(new Son());
GenericClass
g3 = new GenericClass<>();
g3.test(new Son());
}Recursive Generics
Recursive generics are useful when a type must be comparable to itself. The classic example is a max utility that works on any Comparable element.
public class Person implements Comparable
{
private int age;
public Person(int age) { this.age = age; }
public int getAge() { return age; }
@Override
public int compareTo(Person o) { return this.age - o.age; }
}
public class CollectionUtils {
public static
> E max(List
list) {
E result = null;
for (E e : list) {
if (result == null || e.compareTo(result) > 0) {
result = e;
}
}
return result;
}
}
public static void main(String[] args) {
List
persons = new ArrayList<>();
persons.add(new Person(12));
persons.add(new Person(19));
persons.add(new Person(20));
persons.add(new Person(5));
persons.add(new Person(18));
Person oldest = CollectionUtils.max(persons);
}Best Practices
Use plain generics when a method both reads and writes. Apply upper‑bounded wildcards for read‑only parameters and lower‑bounded wildcards for write‑only parameters. Avoid raw types because they discard compile‑time type safety.
Understanding these rules helps you design flexible, reusable APIs and prevents common generic‑related bugs.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.