Java generics let us write classes, interfaces and methods with type parameters, such as List<E>, so the compiler checks the element types for us and our code needs no casts. Generics were added in Java 5, and in Java 25 every collection, stream, Optional and functional interface in the JDK is generic.
We use generics to write one class or method that works with many types and stays type-safe, for example a paged API response that holds books on one page and users on another, or a utility method that returns the largest element of any list.
The following example uses the generic interface List<String>, a generic record Page<T> and a generic method firstOrDefault(), all of which we build in this tutorial.
List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
String first = fruits.get(0); // "apple"
Page<String> page = new Page<>(fruits, 1, 2); // Page[items=[apple, banana], number=1, totalItems=2]
String top = firstOrDefault(page.items(), "none"); // "apple"
String empty = firstOrDefault(List.of(), "none"); // "none"
Notice that fruits.get(0) returns a String without a cast, and fruits.add(42) would not compile. We start with the problem generics solve, write generic classes and methods, restrict type parameters with bounds and wildcards, and see what type erasure leaves at runtime. The last part lists what generics cannot do in Java 25 and the workaround for each rule.
1. Why Java Needs Generics
Before Java 5, a List stored Object references, so it accepted any type, and every read needed a cast. A wrong element was found only when a cast failed at runtime, often far away from the code that added it. Generics move that check to the compiler.

The following example shows the old style, called a raw type, which compiles with an unchecked warning. The second element is an Integer, and the cast to String fails when we read it.
List rawNames = new ArrayList();
rawNames.add("apple");
rawNames.add(42);
String second = (String) rawNames.get(1); // ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
For example, a cache in an online shop keeps product names in a raw List. A new feature adds product IDs to the same list, and the checkout page fails with a ClassCastException in production, two modules away from the bug. With List<String>, the same mistake stops the build.
// does not compile: incompatible types: int cannot be converted to String
List<String> names = new ArrayList<>();
names.add(42);
Raw types exist only for compatibility with code written before Java 5, so new code always passes a type argument. The compiler flags raw types with -Xlint:rawtypes and unchecked calls with -Xlint:unchecked.
2. Generic Classes, Interfaces and Records
A class becomes generic when it declares type parameters in angle brackets after its name. Inside the class, the type parameter works like a normal type in fields, parameters and return types. Every Java collection is declared this way, such as ArrayList<E> and HashMap<K, V>.
2.1. A Generic Record
A REST API that returns lists in pages needs the same wrapper for books, authors and loans. Since Java 16, a record can declare type parameters too, so one Page<T> covers every element type.
public record Page<T>(List<T> items, int number, int totalItems) {
public Page {
items = List.copyOf(items);
}
public boolean isEmpty() {
return items.isEmpty();
}
}
When we create an instance, the type argument replaces T. The diamond operator <>, added in Java 7, lets the compiler infer the type argument from the declared type, and var works with generic types as long as the right side names the type argument.
Page<String> fruitPage = new Page<>(List.of("apple", "banana"), 1, 2);
String firstFruit = fruitPage.items().get(0); // "apple"
var numberPage = new Page<Integer>(List.of(5, 3), 2, 4);
int firstNumber = numberPage.items().get(0); // 5
boolean emptyPage = new Page<String>(List.of(), 3, 4).isEmpty(); // true
2.2. A Generic Interface
A generic interface declares type parameters that each implementation either fixes or passes on. The interface Repository has two of them, the entity type T and the ID type ID, which is how Spring Data declares its repositories.
public interface Repository<T, ID> {
Optional<T> findById(ID id);
void save(T item);
}
public record Book(int id, String title) {
}
public class BookRepository implements Repository<Book, Integer> {
private final Map<Integer, Book> books = new HashMap<>();
@Override
public Optional<Book> findById(Integer id) {
return Optional.ofNullable(books.get(id));
}
@Override
public void save(Book book) {
books.put(book.id(), book);
}
}
BookRepository repository = new BookRepository();
repository.save(new Book(1, "Dune"));
String title = repository.findById(1).map(Book::title).orElse("not found"); // "Dune"
String missing = repository.findById(9).map(Book::title).orElse("not found"); // "not found"
The class BookRepository fixes both type arguments, so its methods take a Book and an Integer. A class can also stay generic and pass the parameters on, as in class InMemoryRepository<T, ID> implements Repository<T, ID>.
2.3. Type Parameter Naming Conventions
Type parameter names are single uppercase letters by convention, so a reader can tell them apart from class names at a glance. The JDK uses the same letters everywhere.
| Letter | Meaning | JDK example |
|---|---|---|
| E | Element of a collection | List<E>, Set<E> |
| K, V | Key and value of a map | Map<K, V> |
| T, U | Any type, first and second | Optional<T>, BiFunction<T, U, R> |
| R | Result type of a function | Function<T, R> |
| N | Number | Common in user code, such as <N extends Number> |
3. Generic Methods and Constructors
A generic method declares its own type parameters before the return type. The parameters belong to that method only, so a generic method can live in a class that is not generic, such as a utility class with static methods. Collections.emptyList() and List.of() are generic methods.
static <T> T firstOrDefault(List<T> items, T fallback) {
return items.isEmpty() ? fallback : items.get(0);
}
The compiler infers T from the arguments and from the target type of the assignment. In the rare case where inference picks the wrong type, we pass a type witness before the method name.
String firstName = firstOrDefault(List.of("apple", "banana"), "none"); // "apple"
Integer firstCount = firstOrDefault(List.of(5, 3), 0); // 5
List<String> noNames = Collections.<String>emptyList(); // []
A constructor can declare type parameters too, independent of the class. The class Price is not generic, but its constructor accepts any Number subtype and converts it to a BigDecimal.
public class Price {
private final BigDecimal amount;
public <N extends Number> Price(N value) {
this.amount = new BigDecimal(value.toString());
}
public BigDecimal getAmount() {
return amount;
}
}
BigDecimal fromInt = new Price(5).getAmount(); // 5
BigDecimal fromDouble = new Price(2.5).getAmount(); // 2.5
4. Bounded Type Parameters
A plain T can be any reference type, so the method body can call only the methods of Object. An upper bound written as T extends SomeType restricts the type arguments to that type and its subtypes. In return, the method body can call the methods of the bound.
For example, a leaderboard service needs the highest score, the latest date or the last name in alphabetical order. All of these types implement Comparable, so one bounded method covers them.
static <T extends Comparable<? super T>> T max(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
int highScore = max(List.of(5, 3, 9)); // 9
String lastName = max(List.of("apple", "banana")); // "banana"
LocalDate latest = max(List.of(LocalDate.of(2026, 1, 5), LocalDate.of(2026, 3, 1))); // 2026-03-01
The bound Comparable<? super T> also accepts types whose compareTo() is declared in a supertype. LocalDate is one of them, because it implements Comparable<ChronoLocalDate>, so T extends Comparable<T> would reject a list of dates.
For example, a volume slider or a discount ratio must stay inside a range. A type parameter can have several bounds joined with &. The first bound can be a class or an interface, and every other bound must be an interface.
public class Ranges {
public static <T extends Number & Comparable<T>> T clamp(T value, T min, T max) {
if (value.compareTo(min) < 0) {
return min;
}
return value.compareTo(max) > 0 ? max : value;
}
}
int volume = Ranges.clamp(15, 0, 10); // 10
double ratio = Ranges.clamp(0.5, 0.0, 1.0); // 0.5
5. Wildcards in Generics
Generic types are invariant. Even though Integer is a subtype of Number, a List<Integer> is not a List<Number>. If the compiler allowed it, code could add a Double through the List<Number> reference into a list that must contain only integers.
// does not compile: incompatible types: ArrayList<Integer> cannot be converted to List<Number>
List<Number> numbers = new ArrayList<Integer>();
Arrays behave differently because they are covariant, so a String[] can be assigned to an Object[]. The array checks every store at runtime and throws an ArrayStoreException, which is the runtime error that generics avoid at compile time.
Object[] items = new String[2];
items[0] = 42; // ArrayStoreException: java.lang.Integer
A wildcard ? stands for an unknown type and makes a method accept a family of parameterized types. The three forms differ in what the method can read from the collection and what it can add to it.
| Wildcard | Accepts | Read elements as | Add elements |
|---|---|---|---|
| List<?> | A list of any type | Object | Only null |
| List<? extends Number> | List<Integer>, List<Double>, List<Number> | Number | Only null |
| List<? super Integer> | List<Integer>, List<Number>, List<Object> | Object | Integer values |
static int countAll(Collection<?> items) {
return items.size();
}
static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number number : numbers) {
total += number.doubleValue();
}
return total;
}
static void addDefaults(List<? super Integer> target) {
target.add(5);
target.add(3);
}
int fruitCount = countAll(Set.of("apple", "banana")); // 2
double intTotal = sum(List.of(5, 3)); // 8.0
double mixedTotal = sum(List.of(2.5, 1.5)); // 4.0
List<Number> target = new ArrayList<>();
addDefaults(target);
int added = target.size(); // 2
A method that only reads from a collection uses ? extends, and a method that only writes into it uses ? super. This rule is known as PECS (producer extends, consumer super), and the JDK applies it in methods such as Collections.copy(List<? super T> dest, List<? extends T> src). Our PECS guide covers the rule with more examples.
6. Type Erasure and What Remains at Runtime
The compiler checks the type arguments and erases them after the check. In the compiled bytecode, every T becomes its leftmost bound, or Object when it has no bound, and the compiler inserts a cast wherever our code reads a T. The JVM never sees List<String>, only List, which kept class files compatible with code compiled before Java 5.
List<String> names = new ArrayList<>();
List<Integer> counts = new ArrayList<>();
boolean sameClass = names.getClass() == counts.getClass(); // true
String className = names.getClass().getName(); // "java.util.ArrayList"
Because the type argument is gone at runtime, instanceof cannot test it. A check against List<?> works, and pattern matching for instanceof binds the result to a variable in one step.
Object value = List.of("apple", "banana");
boolean isList = value instanceof List<?>; // true
int size = value instanceof List<?> list ? list.size() : 0; // 2
// does not compile: Object cannot be safely cast to List<String>
Object value = List.of("apple");
boolean isStringList = value instanceof List<String>;
Erasure removes type arguments from objects and method bodies, but not from declarations. The class file keeps the generic signature of fields, methods and supertypes, and reflection can read it. Libraries such as Jackson and Spring use it to find the element type of a field declared as List<Book>.
public class FruitList extends ArrayList<String> {
}
String superType = FruitList.class.getGenericSuperclass().getTypeName(); // "java.util.ArrayList<java.lang.String>"
Erasure also explains why two overloads that differ only in their type arguments cannot exist in one class. After erasure, both methods have the same signature.
// does not compile: name clash: print(List<Integer>) and print(List<String>) have the same erasure
class Printer {
void print(List<String> names) { }
void print(List<Integer> counts) { }
}
7. Restrictions on Generics and Their Workarounds
Most restrictions follow from erasure, because the JVM does not know T at runtime and cannot create, check or store something of that type. Each rule has a standard workaround, and all of them still apply in Java 25.
| Rule | Compiler error | Workaround |
|---|---|---|
| No primitive type arguments, such as List<int> | unexpected type | Use the wrapper class, such as List<Integer>, or a primitive stream such as IntStream |
| No new T() | unexpected type | Pass a Supplier<T> or a Class<T> |
| No new T[10] | generic array creation | Use a List<T>, or pass an IntFunction<T[]> such as String[]::new |
| No static field of type T | non-static type variable T cannot be referenced from a static context | Make the field an instance field, or use a generic static method |
| No generic exception class | a generic class may not extend java.lang.Throwable | Put the payload in a field of a non-generic exception |
| No instanceof List<String> on an Object | Object cannot be safely cast to List<String> | Test List<?> and check the elements |
Primitive type arguments are the restriction readers meet most. Autoboxing converts between int and wrapper classes such as Integer, so the code reads naturally, but every element is an object on the heap.
// does not compile: unexpected type, required reference, found int
List<int> ids = new ArrayList<>();
List<Integer> ids = new ArrayList<>(List.of(5, 3));
int firstId = ids.get(0); // 5
int idSum = ids.stream().mapToInt(Integer::intValue).sum(); // 8
For new T(), the caller passes the code that creates the object. A Supplier<T> such as ArrayList::new works for any type with a matching constructor, and the JDK uses the same idea in List.toArray(IntFunction) to create arrays of the right type.
public class Pool<T> {
private final Supplier<T> factory;
private final List<T> created = new ArrayList<>();
public Pool(Supplier<T> factory) {
this.factory = factory;
}
public T create() {
T item = factory.get();
created.add(item);
return item;
}
public int size() {
return created.size();
}
}
Pool<StringBuilder> builders = new Pool<>(StringBuilder::new);
StringBuilder builder = builders.create().append("apple"); // apple
int poolSize = builders.size(); // 1
String[] fruitArray = List.of("apple", "banana").toArray(String[]::new); // [apple, banana]
8. Java Generics FAQs
Generic code that compiles on the first try still leaves open points about wildcards, primitive type arguments and runtime type checks.
8.1. What Is the Difference Between <T> and <?>?
A type parameter T names a type, so the method can use it in several places and the compiler keeps them consistent, for example <T> void swap(List<T> list) can read an element and write it back. A wildcard ? means an unknown type that the method never names, so it fits methods that only read elements as Object or only call size() and clear().
8.2. Why Can’t Generics Use Primitive Types?
Erasure replaces T with Object or its bound, and an int is not an Object. So a type argument must be a reference type in Java 25, and we use Integer, Long or Double instead. For large amounts of numbers, int[] or IntStream avoid the boxing cost. Project Valhalla works on lifting this rule, but no released Java version has changed it yet.
8.3. What Is the Difference Between List<Object> and List<?>?
A List<Object> accepts only a List<Object> as an argument, but it allows adding any object. A List<?> accepts a list of any element type, such as List<String>, but allows adding only null, because the compiler does not know the element type.
8.4. Are Generics Checked at Runtime?
No. The compiler checks the type arguments, and the JVM checks only the casts that the compiler inserted. So a raw type or an unchecked cast can put an Integer into a List<String>, and the ClassCastException appears later, when the code reads the element. To get a runtime check at insertion time, we can wrap a collection with Collections.checkedList(list, String.class).
List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
List raw = checked;
raw.add(42); // ClassCastException: Attempt to insert class java.lang.Integer element into collection with element type class java.lang.String
8.5. Can a Static Method Use the Type Parameter of Its Class?
No. A static method belongs to the class, not to an instance such as Page<String>, so the class parameter T is not defined there. A static method declares its own type parameter instead, as in static <T> Page<T> of(List<T> items).
9. Conclusion
Generics let us declare type parameters on classes, interfaces, records, methods and constructors. The compiler uses them to reject wrong types at compile time and to insert the casts we used to write by hand, so a ClassCastException from a collection becomes a compile error.
Bounds such as T extends Comparable<? super T> let a generic method call methods of the bound. Wildcards make methods accept related parameterized types, with ? extends for reading and ? super for writing.
At runtime, type erasure removes the type arguments from objects, which explains the restrictions on primitives, new T(), generic arrays and instanceof. A Supplier<T>, an IntFunction<T[]> or a wrapper class works around each of them.
10. References
- Java Language Specification, Chapter 4 Types, Values, and Variables
- Generics (The Java Tutorials)
- Restrictions on Generics (The Java Tutorials)
- Generics (dev.java)
- List (Java SE 25 API)
Happy Learning !!
in “Generic Type Method or Constructor” you showed how to create a generic method but you didn’t how to call that method.
I am trying this
But it shows an error. Of course.
Java generics does not support primitives
refer here:
https://stackoverflow.com/questions/2721546/why-dont-java-generics-support-primitive-types
Hey, Lokesh,
Thank you for your explanation of the generic types, but I have a question, which might be silly.
I want to define a class with a generic type. For that generic type, I want it to be a Vector or ArrayList.
For example, I want to define it as following:
public class Test<Vector<T>> { ... }This will not compile. What should I do?
Should I simply replace Vector with T, which becomes
pubic class Test<T> { ... }and later when I instantiate this class, replace T with the actual Vector, say, Vector ?
Thanks.
you can say like
public class Test{
…
}
In original collection framework, having homogeneous collections was not possible without adding extra checks before adding some checks in code. Generics were introduced to remove this limitation to be very specific.
i cannot understand this line will you please explain….?
In below example,
listOneis heterogeneous collection (allows any type) whilelistTwois homogeneous collection (only one type).Great job Lokesh! The best I could see when tried different ones including Oracles.
I had tiny hard time when tried to understand “super” for lower bound. I think all that you had great, but if you create one more child class such as GrandGrandChildClass and then pass that list to the the add method to prove that sub class below GrandChildClass is not allowed.
This is not a complete GENERICS tutorial. You missed Generic Inheritance (Subtyping)
Sorry but.. do you mean the other article? PECS
Thank you for the response. “do you mean the other article ?” – No , I am talking about the inhertance in Generic Classes. – Parent child relation in Generics.
Hi Lokesh,
I am unable to understand the difference between these two methods. Can you please help me understand.
void print(List obj) {
Iterator l=obj.iterator();
while(l.hasNext()) {
System.out.println(“inside print1 “+l.next());
}
}
void print2(List obj) {
Iterator l=obj.iterator();
while(l.hasNext()) {
System.out.println(“inside print2 “+l.next());
}
}
both are same, it seems only function name is different .
Hi Lokesh
Great Explanation on Generics concepts. However I have question. Why Generics Type Arrays do not throw ArrayStoreException when we change your code to the below. If we change the type from String to Object then arrays accept heterogeneous types..
because Object class is the parent class of every class.
“For example, say you want to write a method that works on List<String>, List<Integer>, and List<double> you can achieve this by using an upper bounded wildcard e.g. you would specify List<? extends Number>.”
Correction Indeed, because List<String> isn’t the same thing as (or subtype of) List<? extends Number>.
In fact, The common parent in List<?>.
ref: wildcards
In section 5, This is confusing:
Collection<?<, List<? extends Number<, Comparator and Pair
Problem with html? Maybe you mean:
Collection, List, Comparator and Pair
I have several questions .
1. What is the difference when writing and T as return type
2. Wildecard vs T .
3. When use S,U etc. What is exactly mean secondary type
4. Is it possible to explain more detailed what is not allowed by generics . I mean why it is not allowed
Thanks
public static int countAllOccurrences(T[] list, T item) {
………..
return count;
}
countAllOccurrences(new String[]{“1″,”3″,”3″,”6”}, new Integer(2));
This won’t throw any compile error. It just returns count as ‘0’
why generic exceptipn are not allowed?can u elaborate deeply with examples
Comparator cmp = new RuleBasedCollator(new Integer(100)); //String is not superclass of Integer
This is wrong explanation because means something which is super of String (including String itself). So here it should be like: Integer is not superclass of String :)
You are right. Fixed it.
I think there is one type mistake in point no 4 : Genereic Arrays which is “generics use type erasure or remove any type information in runtime” where as Erasure remove Type information at compile time. Am I correct?
then this reason is not applicable when u said “Due to above conflict, instantiating a generic array in java is not permitted.” Please explain
that was because Runtime exception type has no generics information . For more detail , look into JLS 8.1.2
Hi Lokesh,
Good effort. One thing additional which you might want to mention while talking about generics is Joshua Bloch’s PECS ( Producer Extends Consumer Super ).
This reminds when to use ? extends and when to use ? super.
Yes Veer.. I am planning to write a separate pot for PECS only, it’s just so much detailed and interesting topic.
Thanks Sir, You include all these things about generic
Thanks for the post on Generics. It would be very helpful if you can write a post on WebServices
I have written lots of post on RESTful WS. If you need in SOAP, then sorry to disappoint you, i have not worked on SOAP till date, so it will take long time.
Hi Lokesh,
What is not allowed to do with Generics?
ArrayList list = new ArrayList() // allowed
ArrayList list = new ArrayList(); // sub-type not allowed
objects creation using wildcard parameterized type is not allowed
ArrayList list = new ArrayList(); // as told by you a generic class instance creation not allowed