Introduction
Java Generics allow you to write classes, interfaces, and methods that work with different data types while maintaining compile-time type safety.
Before Java 5, collections commonly stored objects as the Object type. Developers then had to manually cast values back to their expected type.
For example:
1import java.util.ArrayList; 2 3public class Main { 4 5 public static void main(String[] args) { 6 7 ArrayList list = new ArrayList(); 8 9 list.add("Java"); 10 list.add(100); 11 12 String language = (String) list.get(1); 13 14 System.out.println(language); 15 } 16}
The code compiles, but it fails at runtime because the second element is an Integer, not a String.
1Exception in thread "main" java.lang.ClassCastException
Generics solve this problem by allowing you to specify the type of data a collection or class is expected to contain.
1ArrayList<String> languages = new ArrayList<>(); 2 3languages.add("Java"); 4languages.add("Python");
Now the compiler prevents incompatible values from being added:
1languages.add(100); // Compile-time error
This is one of the biggest advantages of generics: errors are detected during compilation instead of appearing unexpectedly at runtime.
Generics are extensively used in:
- Java Collections Framework
- Java Streams
- Spring Boot
- Hibernate
- Java APIs
- Enterprise applications
- Generic repositories
- Data-access layers
- Utility classes
In this tutorial, you will learn Java Generics from the fundamentals through practical examples.
Table of Contents
- What Are Generics?
- Why Use Generics?
- Generic Classes
- Multiple Type Parameters
- Generic Methods
- Generic Constructors
- Generic Interfaces
- Unbounded Wildcards
- Upper Bounded Wildcards
- Lower Bounded Wildcards
- Bounded Type Parameters
- Multiple Bounds
- Type Erasure
- Generics vs Object
- Generic Naming Conventions
- Real-World Uses of Generics
- Best Practices
- Practical Generic Project
- Practice Exercises
- Summary
- SEO Keywords
What Are Generics?
Generics are a Java language feature that allows you to define code using type parameters.
Instead of hard-coding one particular data type, a generic class or method can work with different types.
For example:
1class Box<T> { 2 3 private T value; 4 5}
Here, T is a type parameter.
When the class is used, T can represent a specific reference type:
1Box<String> 2Box<Integer> 3Box<Double>
The same class can therefore be reused with different types.
Generic Syntax
A generic class typically looks like this:
1class ClassName<T> { 2 3 // class members 4 5}
Common type parameters include:
| Type Parameter | Common Meaning |
|---|---|
T | Type |
E | Element |
K | Key |
V | Value |
N | Number |
R | Result or Return Type |
These are conventions rather than mandatory names.
You could technically write:
1class Box<MyType> { 2}
However, following standard Java naming conventions makes generic code easier for other developers to understand.
Why Use Generics?
Generics provide several important benefits.
Type Safety
Generics prevent incompatible types from being inserted into generic structures.
1import java.util.ArrayList; 2import java.util.List; 3 4List<String> languages = new ArrayList<>(); 5 6languages.add("Java"); 7languages.add("Python"); 8 9// languages.add(100); // Compile-time error
The compiler knows that the list should contain only String values.
Compile-Time Error Checking
Without generics, some problems are detected only when the application runs.
With generics, many type-related errors are detected while compiling the application.
This makes applications safer and easier to maintain.
No Unnecessary Casting
Without generics:
1Object value = "Java"; 2 3String language = (String) value;
With generics:
1Box<String> box = new Box<>(); 2 3String language = box.getValue();
The compiler already knows that getValue() returns a String.
Code Reusability
A generic class can work with multiple types instead of requiring separate implementations.
For example:
1Box<String> 2Box<Integer> 3Box<Double>
can all use the same Box<T> implementation.
Better API Design
Generics make method parameters and return types clearer.
For example:
1public static <T> T getFirst(List<T> items) { 2 return items.get(0); 3}
The relationship between the input type and return type is clear.
Generic Classes
A generic class contains one or more type parameters.
Basic Generic Class
1class Box<T> { 2 3 private T value; 4 5 public void setValue(T value) { 6 this.value = value; 7 } 8 9 public T getValue() { 10 return value; 11 } 12}
The class does not decide what T means.
The caller determines the type when creating the object.
Using a Generic Class
1public class Main { 2 3 public static void main(String[] args) { 4 5 Box<String> languageBox = new Box<>(); 6 7 languageBox.setValue("Java"); 8 9 String language = languageBox.getValue(); 10 11 System.out.println(language); 12 13 Box<Integer> numberBox = new Box<>(); 14 15 numberBox.setValue(100); 16 17 Integer number = numberBox.getValue(); 18 19 System.out.println(number); 20 } 21}
Output:
1Java 2100
How It Works
When you write:
1Box<String> languageBox = new Box<>();
T becomes String for that particular object.
Therefore:
1setValue(T value)
effectively expects:
1setValue(String value)
and:
1getValue()
returns:
1String
For:
1Box<Integer> numberBox = new Box<>();
T becomes Integer.
Multiple Type Parameters
A generic class can define multiple type parameters.
A common example is a key-value pair.
1class Pair<K, V> { 2 3 private final K key; 4 private final V value; 5 6 public Pair(K key, V value) { 7 this.key = key; 8 this.value = value; 9 } 10 11 public K getKey() { 12 return key; 13 } 14 15 public V getValue() { 16 return value; 17 } 18 19 public void display() { 20 System.out.println(key + " : " + value); 21 } 22}
Using Pair
1public class Main { 2 3 public static void main(String[] args) { 4 5 Pair<Integer, String> student = 6 new Pair<>(101, "Ankit"); 7 8 System.out.println( 9 "ID: " + student.getKey() 10 ); 11 12 System.out.println( 13 "Name: " + student.getValue() 14 ); 15 } 16}
Output:
1ID: 101 2Name: Ankit
Here:
1Pair<Integer, String>
means:
KisIntegerVisString
The same class can be reused:
1Pair<String, Double> product = 2 new Pair<>("Laptop", 79999.0);
Generic Methods
A generic method defines its own type parameter.
The generic type parameter is placed before the return type.
Syntax
1public static <T> T methodName(T value) { 2 3 return value; 4}
Here, <T> declares the method's type parameter.
Example
1public class Utility { 2 3 public static <T> void printValue(T value) { 4 System.out.println(value); 5 } 6 7 public static <T> T getValue(T value) { 8 return value; 9 } 10}
Using the methods:
1public class Main { 2 3 public static void main(String[] args) { 4 5 Utility.printValue("Java"); 6 Utility.printValue(100); 7 Utility.printValue(99.99); 8 9 String language = 10 Utility.getValue("Java"); 11 12 Integer number = 13 Utility.getValue(500); 14 15 System.out.println(language); 16 System.out.println(number); 17 } 18}
Output:
1Java 2100 399.99 4Java 5500
Generic Method with a List
Generic methods become particularly useful when working with collections.
1import java.util.List; 2 3public class ListUtils { 4 5 public static <T> T getFirst(List<T> items) { 6 7 if (items == null || items.isEmpty()) { 8 throw new IllegalArgumentException( 9 "List must not be null or empty." 10 ); 11 } 12 13 return items.get(0); 14 } 15}
Usage:
1import java.util.List; 2 3public class Main { 4 5 public static void main(String[] args) { 6 7 List<String> languages = 8 List.of("Java", "Python", "C++"); 9 10 String first = 11 ListUtils.getFirst(languages); 12 13 System.out.println(first); 14 } 15}
Output:
1Java
The compiler automatically infers the type parameter.
Generic Constructors
A constructor can also declare its own generic type parameter.
1class Display { 2 3 public <T> Display(T value) { 4 System.out.println(value); 5 } 6}
Usage:
1public class Main { 2 3 public static void main(String[] args) { 4 5 new Display("Java"); 6 new Display(100); 7 new Display(99.5); 8 } 9}
Output:
1Java 2100 399.5
Notice that the class itself is not generic.
The constructor independently declares:
1<T>
Generic Interfaces
Interfaces can also use generic type parameters.
For example:
1interface Repository<T> { 2 3 void save(T item); 4 5 T findById(int id); 6}
A concrete implementation can specify the type.
1class StudentRepository 2 implements Repository<Student> { 3 4 @Override 5 public void save(Student student) { 6 System.out.println( 7 "Saving: " + student.getName() 8 ); 9 } 10 11 @Override 12 public Student findById(int id) { 13 return new Student(id, "Ankit"); 14 } 15}
The Student class:
1class Student { 2 3 private final int id; 4 private final String name; 5 6 public Student(int id, String name) { 7 this.id = id; 8 this.name = name; 9 } 10 11 public int getId() { 12 return id; 13 } 14 15 public String getName() { 16 return name; 17 } 18}
Now the repository is type-safe:
1StudentRepository repository = 2 new StudentRepository(); 3 4repository.save( 5 new Student(101, "Ankit") 6);
This pattern is common in application architecture, especially in data-access layers.
Wildcards
Sometimes a method does not need to know the exact generic type.
Java provides the wildcard:
1?
A wildcard represents an unknown type.
For example:
1List<?>
means a list containing some unknown type.
Unbounded Wildcard
1import java.util.List; 2 3public class ListPrinter { 4 5 public static void printList(List<?> items) { 6 7 for (Object item : items) { 8 System.out.println(item); 9 } 10 } 11}
This method can accept lists of different types:
1import java.util.List; 2 3public class Main { 4 5 public static void main(String[] args) { 6 7 List<String> names = 8 List.of("Ankit", "Rahul"); 9 10 List<Integer> numbers = 11 List.of(10, 20, 30); 12 13 ListPrinter.printList(names); 14 ListPrinter.printList(numbers); 15 } 16}
Output:
1Ankit 2Rahul 310 420 530
Why Can We Read as Object?
Because every reference type in Java ultimately derives from Object, values read from List<?> can safely be assigned to Object.
However, you generally cannot add arbitrary values to a List<?> because the actual type is unknown.
Upper Bounded Wildcards
An upper bounded wildcard uses:
1? extends Type
For example:
1List<? extends Number>
means the list contains some type that extends Number.
It could be:
1List<Integer> 2List<Double> 3List<Float>
Example
1import java.util.List; 2 3public class Calculator { 4 5 public static double sum( 6 List<? extends Number> numbers) { 7 8 double total = 0.0; 9 10 for (Number number : numbers) { 11 total += number.doubleValue(); 12 } 13 14 return total; 15 } 16}
Usage:
1import java.util.List; 2 3public class Main { 4 5 public static void main(String[] args) { 6 7 List<Integer> integers = 8 List.of(10, 20, 30); 9 10 List<Double> decimals = 11 List.of(1.5, 2.5, 3.5); 12 13 System.out.println( 14 Calculator.sum(integers) 15 ); 16 17 System.out.println( 18 Calculator.sum(decimals) 19 ); 20 } 21}
Output:
160.0 27.5
The method can read values as Number regardless of the specific numeric subtype.
Lower Bounded Wildcards
A lower bounded wildcard uses:
1? super Type
For example:
1List<? super Integer>
means the list can be a list of Integer, Number, or Object.
Example
1import java.util.List; 2 3public class NumberUtils { 4 5 public static void addNumbers( 6 List<? super Integer> numbers) { 7 8 numbers.add(10); 9 numbers.add(20); 10 numbers.add(30); 11 } 12}
The method can accept:
1List<Integer> 2List<Number> 3List<Object>
Example:
1import java.util.ArrayList; 2import java.util.List; 3 4public class Main { 5 6 public static void main(String[] args) { 7 8 List<Integer> integers = 9 new ArrayList<>(); 10 11 List<Number> numbers = 12 new ArrayList<>(); 13 14 List<Object> objects = 15 new ArrayList<>(); 16 17 NumberUtils.addNumbers(integers); 18 NumberUtils.addNumbers(numbers); 19 NumberUtils.addNumbers(objects); 20 21 System.out.println(integers); 22 System.out.println(numbers); 23 System.out.println(objects); 24 } 25}
Output:
1[10, 20, 30] 2[10, 20, 30] 3[10, 20, 30]
PECS: Producer Extends, Consumer Super
A useful rule for remembering wildcard bounds is PECS:
1Producer → extends 2Consumer → super
If a collection produces values for your code to read:
1List<? extends Number>
If a collection consumes values that your code wants to add:
1List<? super Integer>
This principle is particularly useful when designing flexible generic methods.
Bounded Type Parameters
Sometimes a generic type should be restricted to a particular class hierarchy.
A bounded type parameter uses:
1<T extends Type>
For example:
1<T extends Number>
means T must be Number or a subclass of Number.
Example
1class Calculator<T extends Number> { 2 3 private final T number; 4 5 public Calculator(T number) { 6 this.number = number; 7 } 8 9 public double square() { 10 11 double value = number.doubleValue(); 12 13 return value * value; 14 } 15}
Usage:
1public class Main { 2 3 public static void main(String[] args) { 4 5 Calculator<Integer> integerCalculator = 6 new Calculator<>(5); 7 8 Calculator<Double> doubleCalculator = 9 new Calculator<>(5.5); 10 11 System.out.println( 12 integerCalculator.square() 13 ); 14 15 System.out.println( 16 doubleCalculator.square() 17 ); 18 } 19}
Output:
125.0 230.25
The bound gives the compiler permission to use methods available on Number, such as:
1doubleValue() 2intValue() 3longValue() 4floatValue()
Multiple Bounds
A generic type parameter can have multiple bounds.
For example:
1<T extends Number & Comparable<T>>
The first bound must be a class, while additional bounds can be interfaces.
Example:
1public static <T extends Number & Comparable<T>> 2T maximum(T first, T second) { 3 4 return first.compareTo(second) >= 0 5 ? first 6 : second; 7}
Usage:
1Integer result = 2 maximum(20, 10); 3 4System.out.println(result);
Output:
120
Multiple bounds allow generic code to require multiple capabilities from a type.
Type Erasure
Java implements generics primarily through type erasure.
Generic type information is used by the compiler for type checking, but generic type arguments are generally not retained as runtime type information in the same way as ordinary class metadata.
For example:
1List<String> 2List<Integer>
are different generic types at compile time, but runtime type information does not distinguish these type arguments in the same way.
Type erasure has several important consequences.
Generic Arrays
You cannot directly create:
1new T[10]
inside a generic class.
For example:
1class Storage<T> { 2 3 // T[] items = new T[10]; // Not allowed 4}
Primitive Type Arguments
Generics work with reference types, not primitive types.
This is invalid:
1List<int> numbers;
Use the wrapper type instead:
1List<Integer> numbers;
Java's autoboxing and unboxing make working with wrapper types convenient.
Runtime Type Checks
Because generic type arguments are erased, code such as:
1if (value instanceof List<String>) { 2 // ... 3}
is not allowed.
You can check the raw generic class:
1if (value instanceof List<?>) { 2 // ... 3}
but not the specific type argument at runtime.
Generics vs Object
Consider a collection without generics:
1import java.util.ArrayList; 2 3ArrayList list = new ArrayList(); 4 5list.add("Java"); 6 7String value = (String) list.get(0);
The developer has to perform an explicit cast.
With generics:
1import java.util.ArrayList; 2import java.util.List; 3 4List<String> list = new ArrayList<>(); 5 6list.add("Java"); 7 8String value = list.get(0);
The compiler already knows that the list contains String values.
Comparison
| Without Generics | With Generics |
|---|---|
| Uses raw types | Uses parameterized types |
| Requires explicit casts | Usually no explicit casts |
| Less type-safe | Compile-time type safety |
| Errors can appear at runtime | Many errors detected during compilation |
| Less expressive API | Clearer API contracts |
Real-World Uses of Generics
Generics are everywhere in modern Java development.
Collections
1List<String> names; 2 3Set<Integer> numbers; 4 5Map<Integer, String> students;
Optional
1Optional<String> name;
Comparable
1class Student 2 implements Comparable<Student> { 3 4 @Override 5 public int compareTo(Student other) { 6 return 0; 7 } 8}
Comparator
1Comparator<Student> comparator;
Generic Repositories
1interface Repository<T> { 2 3 void save(T entity); 4 5 T findById(long id); 6}
Generic API Responses
Backend applications frequently define reusable response wrappers:
1class ApiResponse<T> { 2 3 private final T data; 4 private final String message; 5 6 public ApiResponse(T data, String message) { 7 this.data = data; 8 this.message = message; 9 } 10 11 public T getData() { 12 return data; 13 } 14 15 public String getMessage() { 16 return message; 17 } 18}
Now different API responses can use the same structure:
1ApiResponse<String> messageResponse; 2 3ApiResponse<Student> studentResponse; 4 5ApiResponse<List<Student>> studentsResponse;
This is one reason generics are so important in backend Java development.
Generic Utility Example
A practical generic utility can return the last element of a list.
1import java.util.List; 2 3public final class CollectionUtils { 4 5 private CollectionUtils() { 6 // Prevent object creation. 7 } 8 9 public static <T> T getLast(List<T> items) { 10 11 if (items == null || items.isEmpty()) { 12 throw new IllegalArgumentException( 13 "List must not be null or empty." 14 ); 15 } 16 17 return items.get(items.size() - 1); 18 } 19}
Usage:
1import java.util.List; 2 3public class Main { 4 5 public static void main(String[] args) { 6 7 List<String> languages = 8 List.of("Java", "Python", "C++"); 9 10 List<Integer> numbers = 11 List.of(10, 20, 30); 12 13 String language = 14 CollectionUtils.getLast(languages); 15 16 Integer number = 17 CollectionUtils.getLast(numbers); 18 19 System.out.println(language); 20 System.out.println(number); 21 } 22}
Output:
1C++ 230
The same method works for different types without duplicating the implementation.
Best Practices
Use Generics with Collections
Prefer:
1List<String> names = new ArrayList<>();
instead of:
1List names = new ArrayList();
Raw types remove much of the compile-time type safety provided by generics.
Prefer the Diamond Operator
Instead of:
1List<String> names = 2 new ArrayList<String>();
prefer:
1List<String> names = 2 new ArrayList<>();
The compiler can infer the type argument.
Use Appropriate Bounds
If a method only needs to read numeric values, an upper bound can make the API more flexible:
1List<? extends Number>
If a method needs to add Integer values:
1List<? super Integer>
Keep Generic APIs Simple
Do not introduce complex generic parameters unless they provide a real benefit.
Good generic code should make an API more reusable without making it unnecessarily difficult to understand.
Avoid Raw Types
Avoid:
1List list; 2Map map;
Prefer:
1List<String> names; 2Map<Integer, String> students;
Use Meaningful Type Parameters
Use conventional names when they communicate the purpose clearly:
1<T> 2<E> 3<K, V>
For more complex APIs, descriptive names can sometimes improve readability.
Practice Project: Generic Storage
Let's build a reusable generic storage class.
1class Storage<T> { 2 3 private T item; 4 5 public void store(T item) { 6 this.item = item; 7 } 8 9 public T retrieve() { 10 return item; 11 } 12 13 public boolean isEmpty() { 14 return item == null; 15 } 16}
Using the Generic Storage
1public class Main { 2 3 public static void main(String[] args) { 4 5 Storage<String> languageStorage = 6 new Storage<>(); 7 8 languageStorage.store("Java"); 9 10 System.out.println( 11 languageStorage.retrieve() 12 ); 13 14 Storage<Integer> numberStorage = 15 new Storage<>(); 16 17 numberStorage.store(500); 18 19 System.out.println( 20 numberStorage.retrieve() 21 ); 22 23 System.out.println( 24 "Storage empty: " 25 + numberStorage.isEmpty() 26 ); 27 } 28}
Output:
1Java 2500 3Storage empty: false
The same Storage<T> class can store different reference types.
Practice Project: Generic Repository
Create a generic repository interface:
1interface Repository<T> { 2 3 void save(T item); 4 5 T findById(int id); 6 7 void delete(T item); 8}
Create a student model:
1class Student { 2 3 private final int id; 4 private final String name; 5 6 public Student(int id, String name) { 7 this.id = id; 8 this.name = name; 9 } 10 11 public int getId() { 12 return id; 13 } 14 15 public String getName() { 16 return name; 17 } 18 19 @Override 20 public String toString() { 21 return "Student{id=" + id 22 + ", name='" + name + "'}"; 23 } 24}
A simple implementation can use a list:
1import java.util.ArrayList; 2import java.util.List; 3 4class StudentRepository 5 implements Repository<Student> { 6 7 private final List<Student> students = 8 new ArrayList<>(); 9 10 @Override 11 public void save(Student student) { 12 students.add(student); 13 } 14 15 @Override 16 public Student findById(int id) { 17 18 for (Student student : students) { 19 20 if (student.getId() == id) { 21 return student; 22 } 23 } 24 25 return null; 26 } 27 28 @Override 29 public void delete(Student student) { 30 students.remove(student); 31 } 32}
Usage:
1public class Main { 2 3 public static void main(String[] args) { 4 5 StudentRepository repository = 6 new StudentRepository(); 7 8 repository.save( 9 new Student(101, "Ankit") 10 ); 11 12 repository.save( 13 new Student(102, "Rahul") 14 ); 15 16 Student student = 17 repository.findById(101); 18 19 System.out.println(student); 20 } 21}
Output:
1Student{id=101, name='Ankit'}
This project demonstrates how generics can be used to create reusable type-safe application components.
Practice Exercises
Exercise 1: Generic Stack
Create a generic Stack<T> class with:
push(T item)pop()peek()isEmpty()size()
Test it with both String and Integer.
Exercise 2: Generic Pair
Create a Pair<K, V> class that stores:
- A key
- A value
Add methods to:
- Get the key
- Get the value
- Update the value
- Display both values
Exercise 3: Generic Calculator
Create:
1Calculator<T extends Number>
Add methods to calculate:
- Square
- Cube
- Double value
- Integer value
Test the class with Integer and Double.
Exercise 4: Generic Repository
Create:
1Repository<T>
with methods:
1save(T item) 2findById(int id) 3delete(T item)
Implement the repository for a Student class.
Exercise 5: Generic Utility Methods
Create a utility class with generic methods for:
- Finding the first element
- Finding the last element
- Swapping two elements
- Printing a list
- Checking whether a list is empty
Exercise 6: Student Management System
Build a small student management application that:
- Stores students using
List<Student> - Uses a generic
Repository<Student> - Searches students by ID
- Displays all students
- Deletes students
- Uses generic utility methods
- Demonstrates bounded types where appropriate
Common Mistakes with Generics
Using Raw Collections
Avoid:
1List students = new ArrayList();
Prefer:
1List<Student> students = new ArrayList<>();
Confusing extends with super
Remember:
1? extends T
is generally useful when consuming values from a producer.
1? super T
is generally useful when supplying values to a consumer.
The PECS rule helps remember this:
1Producer Extends 2Consumer Super
Using Primitive Types
This is invalid:
1List<int> numbers;
Use:
1List<Integer> numbers;
Overcomplicating Generic APIs
Generics should improve reusability and type safety. If a generic design becomes unnecessarily complicated, simplify it.
Summary
In this Java Generics tutorial, you learned how generics make Java programs more reusable, readable, and type-safe.
You learned:
- What Java Generics are
- Why generics are useful
- How generic classes work
- How to create classes with multiple type parameters
- How to create generic methods
- How generic constructors work
- How to create generic interfaces
- How unbounded wildcards work
- How
? extendsworks - How
? superworks - How bounded type parameters work
- How multiple bounds work
- What type erasure means
- Why generic arrays and primitive type arguments have restrictions
- The difference between generics and raw
Objecttypes - Common generic naming conventions
- How generics are used in collections and backend applications
- How to create reusable generic utilities
- How to build a generic repository
Generics are a fundamental part of modern Java programming. You will encounter them frequently when working with collections, streams, repositories, APIs, Spring Boot, Hibernate, and other Java frameworks.
A strong understanding of generics will also make advanced Java topics such as Collections, Lambda Expressions, Streams, Functional Programming, and Spring Boot easier to understand.
Practice Challenge
Build a complete generic data-management system.
Your application should contain:
1GenericRepository<T> 2 | 3 +── save() 4 +── findById() 5 +── findAll() 6 +── delete()
Then implement it for:
1Student 2Employee 3Product
The goal is to use one generic repository design for multiple domain models while maintaining compile-time type safety.