Fundamentals 17 min read

Understanding Java Generics: Basics, Wildcards, Bounds, and Type Erasure

This article introduces Java generics, covering generic classes and methods, wildcard usage with the PECS principle, bounded type parameters, and the implications of type erasure, illustrating each concept with code examples and discussing common pitfalls such as generic arrays, bridge methods, and runtime type checks.

Java Captain
Java Captain
Java Captain
Understanding Java Generics: Basics, Wildcards, Bounds, and Type Erasure

Java generics, introduced in JDK 1.5, provide compile‑time type checking that helps catch illegal type usage early; they are heavily used throughout the Java Collections Framework.

Generic class example : a non‑generic

public class Box { private String object; public void set(String object) { this.object = object; } public String get() { return object; } }

can store only String values, while the generic version

public class Box<T> { private T t; public void set(T t) { this.t = t; } public T get() { return t; } }

can be instantiated as Box<Integer> integerBox = new Box<Integer>();, Box<Double> doubleBox = new Box<Double>();, Box<String> stringBox = new Box<String>();.

Generic method example : declaring a method with type parameters before the return type, e.g.

public class Util { public static <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } }

together with

public class Pair<K,V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }

. Calls can use explicit type arguments boolean same = Util.<Integer,String>compare(p1,p2); or rely on type inference boolean same = Util.compare(p1,p2);.

Bounded type parameters : a naïve method

public static <T> int countGreaterThan(T[] anArray, T elem) { int count=0; for(T e:anArray) if(e > elem) ++count; return count; }

fails because the > operator is not defined for arbitrary types. By constraining T to Comparable we can write

public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) { int count=0; for(T e:anArray) if(e.compareTo(elem) > 0) ++count; return count; }

.

Wildcards and the PECS principle : a method public void boxTest(Box<Number> n) { /* ... */ } cannot accept Box<Integer> or Box<Double> because generic types are invariant. Using wildcards, a covariant reader can be defined as

static class CovariantReader<T> { T readCovariant(List<? extends T> list) { return list.get(0); } }

and used with

CovariantReader<Fruit> fruitReader = new CovariantReader<>();

to read from List<Fruit> or List<Apple>. The PECS rule states “Producer extends, Consumer super”: List<? extends Fruit> flist = new ArrayList<Apple>(); allows reading but not adding (except null), while List<? super Fruit> flist = new ArrayList<Object>(); permits adding Fruit instances but not reading specific subtypes. The standard library method

public static <T> void copy(List<? super T> dest, List<? extends T> src) { for(int i=0;i<src.size();i++) dest.set(i, src.get(i)); }

combines both forms.

Type erasure : generic type information is removed after compilation. A class

public class Node<T> { private T data; private Node<T> next; public Node(T data, Node<T> next) { this.data = data; this.next = next; } public T getData() { return data; } }

is compiled to

public class Node { private Object data; private Node next; public Node(Object data, Node next) { this.data = data; this.next = next; } public Object getData() { return data; } }

. Adding a bound, e.g. class Node<T extends Comparable<T>> { … }, changes the erased type to Comparable instead of Object.

Problems caused by erasure :

Generic arrays are prohibited:

List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile‑time error

because the runtime cannot distinguish the element type.

Bridge methods are generated to preserve polymorphism, as shown by

class MyNode extends Node<Integer> { public void setData(Integer data) { … } // bridge: public void setData(Object data) { setData((Integer) data); } }

, which can lead to ClassCastException when a raw Node reference receives an incompatible value.

Instance creation with a type parameter is illegal ( E elem = new E();), but can be achieved via reflection:

public static <E> void append(List<E> list, Class<E> cls) throws Exception { E elem = cls.newInstance(); list.add(elem); }

.

The instanceof operator cannot be used with concrete generic types; using a wildcard makes it reifiable: if(list instanceof ArrayList<?>) { … }.

These examples demonstrate both the power and the limitations of Java generics, emphasizing the need to understand bounds, wildcards, and type erasure when designing type‑safe APIs.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ReflectionGenericsPECSType ErasureWildcards
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.