Generics Fundamentals
By the Syncfusion DevTutorials team · Last updated
Write type-safe, reusable code with generic classes, methods and interfaces, bounded type parameters, wildcards, and generic collections.
Think about a lunch box with a label on the lid.
One box says "Sandwiches". Another says "Fruit". The box itself is the same design every time. Only the label changes. And because of the label, nobody puts soup in the sandwich box by mistake.
Generics work the same way. You write a class or method once, like the lunch box design. Then, each time you use it, you add a label that says what type it holds: Box<String>, Box<Integer>. Java checks the label and refuses the wrong type.
In simple words: generics let you write code once that works with many types, while Java still checks that each type is used correctly.
What You Will Learn
In this lesson, you will learn:
- What generics are, and the problems they solve.
- How to write generic classes, methods and interfaces.
- What type parameters like
TandEmean. - How bounded type parameters limit which types are allowed.
- How the wildcards
?,? extendsand? superwork. - How generic collections like
List<String>keep data safe. - How generic collections compare with the old, non-generic ones.
Start With a Working Example
Press Run.
One Box class, three different types
public class Main {
public static void main(String[] args) {
Box<String> nameBox = new Box<>("Asha");
Box<Integer> ageBox = new Box<>(21);
Box<Double> priceBox = new Box<>(499.5);
String name = nameBox.get();
int age = ageBox.get();
double price = priceBox.get();
System.out.println(name + " is " + age);
System.out.println("Price: " + price);
}
}
class Box<T> {
private T item;
Box(T item) {
this.item = item;
}
T get() {
return item;
}
}
Expected output:
Asha is 21
Price: 499.5
Box is written once. The <T> after its name is a placeholder for a type. Each time you create a box, you fill in the real type: String, Integer or Double.
nameBox.get() returns a String, with no cast needed. Java already knows what is inside.
Try This
Add the line nameBox = new Box<>(42);. The program no longer compiles, because nameBox is labelled Box<String> and 42 is not a String.
Introduction to Generics
Generics let a class, interface or method work with a type that is chosen later, when the code is used.
You mark the "chosen later" type with a type parameter in angle brackets, such as <T>. When you use the code, you replace it with a real type, called a type argument.
class Box<T> { ... } // T is a type parameter
Box<String> b = ...; // String is the type argument
You have already used generics without knowing:
ArrayList<String>is a list that only holds strings.HashMap<String, Integer>maps strings to whole numbers.Predicate<Integer>tests whole numbers.
Generics were added in Java 5. Today, almost every collection and many other classes use them.
Why Use Generics
Without generics, a general-purpose class has to use Object, the parent of every class. It can hold anything. That sounds handy, but it causes two problems.
Problem 1: Casts Everywhere
When you take a value out of an Object container, Java only knows it is an Object. You must cast it back to the real type yourself.
Problem 2: Mistakes Found Too Late
Nothing stops you putting the wrong type in. The mistake only shows up later, when the program runs and the cast fails with a ClassCastException.
Without generics, every read needs a cast
public class Main {
public static void main(String[] args) {
ObjectBox box = new ObjectBox("Ravi");
String name = (String) box.get();
System.out.println("Name: " + name);
Object raw = box.get();
System.out.println("Is it a String? " + (raw instanceof String));
}
}
class ObjectBox {
private Object item;
ObjectBox(Object item) {
this.item = item;
}
Object get() {
return item;
}
}
Expected output:
Name: Ravi
Is it a String? true
box.get() returns an Object, so the code must cast it with (String). If someone had stored an Integer instead, that cast would fail while the program runs.
With generics, the same mistake is caught by the compiler, before the program ever runs. Read the code, then look at the output below it.
Generics catch the wrong type at compile time
public class Main {
public static void main(String[] args) {
Box<String> box = new Box<>("Ravi");
box.set(42);
}
}
class Box<T> {
private T item;
Box(T item) { this.item = item; }
void set(T item) { this.item = item; }
T get() { return item; }
}
Output:
Main.java:4: error: incompatible types: int cannot be converted to String
box.set(42);
^
1 error
The compiler reports incompatible types: int cannot be converted to String.
The Benefits
| Benefit | What it means |
|---|---|
| Type safety | The compiler rejects the wrong type, before the program runs |
| No casts | Values come out already as the right type |
| Reuse | One class or method works with many types |
| Clear code | List<Student> tells the reader exactly what the list holds |
Generic Classes
A generic class has one or more type parameters after its name.
class ClassName<T> {
// T can be used as a type inside the class
}
Inside the class, T works like a real type. You can use it for fields, parameters, return types and local variables.
The Diamond Operator
When you create an object, you can leave the angle brackets on the right side empty: new Box<>("Asha"). Java works out the type from the left side. The empty <> is called the diamond operator.
Several Type Parameters
A class can have more than one type parameter, separated by commas.
A Pair holds two values of different types
public class Main {
public static void main(String[] args) {
Pair<String, Integer> score = new Pair<>("Asha", 92);
Pair<String, String> capital = new Pair<>("India", "New Delhi");
Pair<Integer, Boolean> check = new Pair<>(17, false);
System.out.println(score.getFirst() + " scored " + score.getSecond());
System.out.println("Capital of " + capital.getFirst() + ": " + capital.getSecond());
System.out.println(check.getFirst() + " is even? " + check.getSecond());
}
}
class Pair<K, V> {
private final K first;
private final V second;
Pair(K first, V second) {
this.first = first;
this.second = second;
}
K getFirst() { return first; }
V getSecond() { return second; }
}
Expected output:
Asha scored 92
Capital of India: New Delhi
17 is even? false
Pair<K, V> has two type parameters. Each pair chooses its own two types.
Type arguments must be classes, not primitives. Write Box<Integer>, not Box<int>. Java turns int values into Integer objects and back automatically, which is called autoboxing, so new Box<>(21) still works.
| Primitive | Wrapper class to use in generics |
|---|---|
int | Integer |
double | Double |
char | Character |
boolean | Boolean |
long | Long |
Generic Methods
A generic method has its own type parameter, declared before the return type.
static <T> void printTwice(T value) { ... }
The method can be inside an ordinary, non-generic class. Its type parameter only exists inside that one method.
When you call it, you usually do not write the type. Java works it out from the arguments. This is called type inference.
One method works with any type of array
public class Main {
public static void main(String[] args) {
String[] cities = {"Chennai", "Pune", "Kochi"};
Integer[] marks = {72, 88, 95};
Character[] grades = {'A', 'B', 'A'};
printAll(cities);
printAll(marks);
printAll(grades);
System.out.println("Last city: " + lastItem(cities));
System.out.println("Last mark: " + lastItem(marks));
}
static <T> void printAll(T[] items) {
for (T item : items) {
System.out.print(item + " ");
}
System.out.println();
}
static <T> T lastItem(T[] items) {
return items[items.length - 1];
}
}
Expected output:
Chennai Pune Kochi
72 88 95
A B A
Last city: Kochi
Last mark: 95
printAll is written once and works on three different array types.
lastItem returns T. When it gets a String[], it returns a String. When it gets an Integer[], it returns an Integer. No cast is needed either way.
Generic Interfaces
An interface can have type parameters too. Each class that implements it chooses the real type.
Java's own Comparable<T> is a generic interface. A class implements Comparable<Student> to say "students can be compared with other students".
Each class picks the type for a generic interface
public class Main {
public static void main(String[] args) {
Repository<String> names = new NameStore();
names.save("Asha");
names.save("Ravi");
System.out.println("Names: " + names.count());
Repository<Integer> scores = new ScoreStore();
scores.save(85);
System.out.println("Scores: " + scores.count());
}
}
interface Repository<T> {
void save(T item);
int count();
}
class NameStore implements Repository<String> {
private int total = 0;
public void save(String item) {
System.out.println("Saving name " + item);
total++;
}
public int count() {
return total;
}
}
class ScoreStore implements Repository<Integer> {
private int total = 0;
public void save(Integer item) {
System.out.println("Saving score " + item);
total++;
}
public int count() {
return total;
}
}
Expected output:
Saving name Asha
Saving name Ravi
Names: 2
Saving score 85
Scores: 1
NameStore fills in T with String, so its save method takes a String. ScoreStore fills in T with Integer.
A class can also stay generic itself: class MemoryStore<T> implements Repository<T> works for any type.
Type Parameters
A type parameter is the placeholder name inside the angle brackets.
By convention, type parameters are single capital letters. This makes them easy to tell apart from real class names.
| Letter | Usually means | Example |
|---|---|---|
T | Type | Box<T> |
E | Element (of a collection) | List<E> |
K | Key | Map<K, V> |
V | Value | Map<K, V> |
N | Number | Calculator<N> |
R | Return type | Function<T, R> |
S, U | Second and third types | Pair<T, U> |
These are only conventions. Box<Thing> would compile, but it looks like a real class called Thing, which is confusing.
Where a Type Parameter Can Be Used
Inside a generic class, T can be the type of:
- a field:
private T item; - a parameter:
void set(T item) - a return value:
T get() - a local variable:
T copy = item;
You cannot:
- create an object of it:
new T()does not compile; - use it in a static field:
static T shared;does not compile, because static members belong to the class, not to oneBox<String>orBox<Integer>.
Bounded Type Parameters
Sometimes a method only makes sense for certain types. Adding up values only works for numbers. Comparing values only works for things that can be compared.
A bounded type parameter limits the allowed types with extends:
<T extends Number>
This means "T can be Number or any child of Number", such as Integer, Double or Long.
Because Java now knows T is some kind of Number, you can call Number methods on it, such as doubleValue().
Only numbers are allowed, so number methods can be used
public class Main {
public static void main(String[] args) {
Integer[] marks = {70, 85, 90};
Double[] prices = {19.5, 30.0, 50.5};
System.out.println("Average mark: " + average(marks));
System.out.println("Average price: " + average(prices));
}
static <T extends Number> double average(T[] values) {
double total = 0;
for (T v : values) {
total += v.doubleValue();
}
return total / values.length;
}
}
Expected output:
Average mark: 81.66666666666667
Average price: 33.333333333333336
average accepts an Integer[] and a Double[]. Calling it with a String[] does not compile, because String is not a Number.
Without the bound, v.doubleValue() would not compile. Java would only know v is some unknown type.
Bounding by an Interface
The bound can be an interface too. For interfaces, you still write extends, not implements.
T must be Comparable, so compareTo can be used
public class Main {
public static void main(String[] args) {
System.out.println(largest(42, 17, 99, 63));
System.out.println(largest("mango", "apple", "kiwi"));
System.out.println(largest(3.5, 2.25));
}
@SafeVarargs
static <T extends Comparable<T>> T largest(T... items) {
T best = items[0];
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
}
Expected output:
99
mango
3.5
T extends Comparable<T> means "any type that can be compared with itself". Integer, String and Double all qualify.
"mango" is the largest string, because it comes last in dictionary order.
@SafeVarargs tells the compiler this varargs method is safe with generics, which hides a warning.
A type can have several bounds, joined with &: <T extends Number & Comparable<T>>. A class bound must come first.
Wildcards
A wildcard, written ?, stands for "some type I do not need to name". It is used in variable and parameter types, most often for method parameters.
Why are wildcards needed? Because of a surprising rule:
List<Integer> is NOT a List<Number>
Even though Integer is a child of Number, a list of integers is not a child of a list of numbers. If it were, you could add a Double to a list that must only hold integers.
So a method that takes List<Number> will not accept a List<Integer>. Wildcards solve this.
Unbounded Wildcard (?)
List<?> means "a list of some type, I do not care which".
Use it when the method only needs things every object has, such as size() or toString().
You can read items from a List<?>, but only as Object. You cannot add anything except null, because Java does not know which type is allowed.
Upper-Bounded Wildcard (? extends)
List<? extends Number> means "a list of Number, or of any child of Number".
It accepts List<Integer>, List<Double> and List<Number>.
You can read items as Number. You cannot add items, because the list might really be a List<Integer>, and a Double would not belong.
Lower-Bounded Wildcard (? super)
List<? super Integer> means "a list of Integer, or of any parent of Integer".
It accepts List<Integer>, List<Number> and List<Object>.
You can add Integer values, because every one of those lists can hold an Integer. When you read, you only get Object.
Three wildcards, three different jobs
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> marks = List.of(70, 85, 90);
List<Double> prices = List.of(19.5, 30.5);
List<String> names = List.of("Asha", "Ravi");
printSize(marks);
printSize(names);
System.out.println("Sum of marks: " + sum(marks));
System.out.println("Sum of prices: " + sum(prices));
List<Number> numbers = new ArrayList<>();
addTens(numbers);
List<Object> things = new ArrayList<>();
addTens(things);
System.out.println("Numbers: " + numbers);
System.out.println("Things: " + things);
}
static void printSize(List<?> list) {
System.out.println("List has " + list.size() + " items");
}
static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
static void addTens(List<? super Integer> list) {
list.add(10);
list.add(20);
}
}
Expected output:
List has 3 items
List has 2 items
Sum of marks: 245.0
Sum of prices: 50.0
Numbers: [10, 20]
Things: [10, 20]
printSizeworks on any list, because it only asks for the size.sumreads numbers from a list of integers and from a list of doubles.addTenswrites integers into a list of numbers and into a list of objects.
PECS: Producer Extends, Consumer Super
A simple rule tells you which wildcard to choose:
- If the list produces values for you to read, use
extends. - If the list consumes values you write into it, use
super. - If you do neither, use
?on its own.
| Wildcard | Accepts | Can read as | Can add |
|---|---|---|---|
List<?> | A list of any type | Object | Only null |
List<? extends Number> | Number or its children | Number | Nothing (except null) |
List<? super Integer> | Integer or its parents | Object | Integer values |
Easy Way to Remember: PECS, Producer Extends, Consumer Super. Reading out? extends. Putting in? super.
Generic Collections
The Collections Framework in java.util is Java's set of ready-made data structures. Every one of them is generic.
| Collection | What it holds | Example |
|---|---|---|
List<E> / ArrayList<E> | An ordered list, duplicates allowed | List<String> names |
Set<E> / HashSet<E> | Unique items, no duplicates | Set<String> tags |
Map<K, V> / HashMap<K, V> | Key-value pairs | Map<String, Integer> stock |
Queue<E> / ArrayDeque<E> | Items in arrival order | Queue<String> tickets |
You choose the element type in angle brackets. The collection then accepts and returns only that type.
A list, a set and a map, each with its own types
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Main {
public static void main(String[] args) {
List<String> orders = new ArrayList<>();
orders.add("Pen");
orders.add("Bag");
orders.add("Pen");
System.out.println("Orders: " + orders);
Set<String> uniqueItems = new HashSet<>(orders);
System.out.println("Different items: " + uniqueItems.size());
Map<String, Integer> prices = new HashMap<>();
prices.put("Pen", 20);
prices.put("Bag", 499);
int total = 0;
for (String item : orders) {
total += prices.get(item);
}
System.out.println("Total: " + total);
}
}
Expected output:
Orders: [Pen, Bag, Pen]
Different items: 2
Total: 539
prices.get(item) returns an Integer, not an Object. So it can be added to total straight away, with no cast.
You will study each collection in depth in the next chapter.
It is good practice to use the interface type on the left, like List<String> names = new ArrayList<>();. Then you can switch to another kind of list later by changing only the right side.
Generics vs Non-Generic Collections
Before Java 5, collections had no type parameter. They held Object. Using a collection without its type argument is called a raw type:
ArrayList list = new ArrayList(); // raw type: holds anything
Raw types still compile, so old code keeps working. But you lose every benefit of generics, and the compiler warns you about unchecked operations. Read the code, then look at the output below it.
A raw list accepts anything, a generic list does not
import java.util.ArrayList;
import java.util.List;
public class Main {
@SuppressWarnings({"rawtypes", "unchecked"})
public static void main(String[] args) {
List rawList = new ArrayList();
rawList.add("Asha");
rawList.add(42);
rawList.add(true);
System.out.println("Raw list: " + rawList);
String first = (String) rawList.get(0);
System.out.println("Needed a cast to read: " + first);
List<String> safeList = new ArrayList<>();
safeList.add("Asha");
safeList.add("Ravi");
String name = safeList.get(0);
System.out.println("No cast needed: " + name);
}
}
Output:
Raw list: [Asha, 42, true]
Needed a cast to read: Asha
No cast needed: Asha
The raw list happily held a String, an int and a boolean. Reading from it needed a cast. If the code had cast rawList.get(1) to String, the program would have stopped with a ClassCastException.
safeList only accepts strings, and reading from it needs no cast. safeList.add(42) would not compile.
@SuppressWarnings hides the compiler's warnings for this demonstration. In your own code, do not hide them. Fix the raw type instead.
| Non-generic (raw) collection | Generic collection | |
|---|---|---|
| Written as | ArrayList list | ArrayList<String> list |
| Holds | Any object | Only the chosen type |
| Wrong type added | Allowed | Compile error |
| Reading a value | Needs a cast | No cast |
| Errors found | While the program runs (ClassCastException) | When the code is compiled |
| Compiler warnings | Yes, "unchecked" | No |
| Use today? | Only in very old code | Always |
Common mistakes
The following mistakes are common when writing generic classes and methods. Each row shows what you see, why it happens and how to fix it.
| Symptom | Cause | Fix |
|---|---|---|
unexpected type; required: reference, found: int | A primitive was used as a type argument | Use the wrapper class, like Integer |
List<Integer> cannot be converted to List<Number> | Generic types do not follow inheritance | Use List<? extends Number> |
| Cannot add to a list parameter | The parameter uses ? extends | Use ? super for lists you write into |
unexpected type on new T() | Type parameters cannot be instantiated | Pass in a Supplier<T> or a Class<T> |
cannot find symbol on a method of T | T has no bound | Add a bound: <T extends Number> |
| "unchecked" warnings | A raw type was used | Add the type argument |
What to Remember
- Generics let you write a class, interface or method once and use it with many types, safely.
- The compiler checks the type, so wrong types are caught early and no casts are needed.
- Type parameters, like
T,E,KandV, are placeholders. Type arguments must be classes, likeInteger. - Generic methods declare
<T>before the return type. Java infersTfrom the arguments. - Bounds like
<T extends Number>limit the allowed types and unlock their methods. - Wildcards:
?for any type,? extendsto read,? superto write (PECS). - Always use generic collections like
List<String>. Raw types are only for very old code.
Next: Collections Fundamentals