Generics
java generics type safety wildcard extends super PECS type erasure bounded generic class method diamond operator
Introduction
In the previous lessons, you learned about:
- Collections Framework
- Streams API
- Lambda Expressions
- Multithreading
Consider the following code:
ArrayList list = new ArrayList();list.add("Java");list.add(100);list.add(true);
This code compiles successfully because the collection is using a raw type.
However, later:
String language = (String) list.get(1);
Output:
Exception in thread "main"java.lang.ClassCastException
The problem is that the compiler cannot verify the type of elements stored in the collection.
Java solves this problem using Generics.
Generics provide compile-time type safety, eliminate unnecessary casting, and make code reusable.
Introduced in Java 5, Generics are one of the most important features used throughout the Java ecosystem.
Generics are extensively used in:
- Java Collections Framework
- Streams API
- Spring Boot
- Hibernate
- JPA
- Kafka
- REST APIs
- Microservices
- Java Concurrency APIs
Understanding Generics is essential for writing modern, type-safe Java applications.
In this lesson, you'll learn:
- What are Generics?
- Why Generics?
- Generic Classes
- Generic Methods
- Multiple Type Parameters
- Bounded Type Parameters
- Wildcards
- Upper Bounded Wildcards
- Lower Bounded Wildcards
- Type Erasure
- Generic Collections
- PECS Principle
- Best Practices
- Common Mistakes
- Hands-on Exercises
- Professional Interview Questions
What are Generics?
Generics allow classes, interfaces, and methods to operate on different data types while maintaining compile-time type safety.
Instead of writing:
ArrayList list =new ArrayList();
Write:
List<String> list =new ArrayList<>();
Now only String objects can be added.
Why Generics?
Without Generics:
List list =new ArrayList();list.add("Java");list.add(100);
Later:
String text =(String) list.get(1);
Runtime Error.
With Generics:
List<String> list =new ArrayList<>();list.add("Java");// Compilation Errorlist.add(100);
The compiler catches the error before the program runs.
Benefits:
- Compile-time type safety
- No explicit casting
- Cleaner code
- Better IDE support
- Code reusability
Generic Class
Example:
class Box<T> {private T value;public void set(T value){this.value = value;}public T get(){return value;}}
Using it:
Box<String> box =new Box<>();box.set("Java");System.out.println(box.get());
Output:
Java
Generic Class with Different Types
Box<Integer> numberBox =new Box<>();numberBox.set(100);Box<Double> decimalBox =new Box<>();decimalBox.set(99.99);
The same class works with multiple data types.
Multiple Type Parameters
A class can have multiple generic types.
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;}}
Using it:
Pair<Integer,String> employee =new Pair<>(101,"Rahul");
Generic Methods
Generics can also be applied to methods.
public static <T> void print(T value){System.out.println(value);}
Calling:
print("Java");print(100);print(true);
Output:
Java100true
Generic Interfaces
Example:
interface Repository<T>{void save(T entity);T findById(int id);}
Implementation:
class EmployeeRepositoryimplements Repository<Employee>{@Overridepublic void save(Employee entity){}@Overridepublic Employee findById(int id){return new Employee();}}
Bounded Type Parameters
Sometimes we want to restrict generic types.
Example:
class Calculator<T extends Number>{public double square(T value){return value.doubleValue() *value.doubleValue();}}
Allowed:
Calculator<Integer> c1 =new Calculator<>();Calculator<Double> c2 =new Calculator<>();
Not Allowed:
Calculator<String> c3 =new Calculator<>();
Compilation Error.
Multiple Bounds
<T extends Number & Comparable<T>>
The type must:
- Extend
Number - Implement
Comparable
Wildcards
Wildcard:
?
Represents an unknown type.
Example:
List<?> list;
Useful when the exact type is not important.
Upper Bounded Wildcards
List<? extends Number>
Accepts:
- Integer
- Double
- Float
- Long
Example:
public void printNumbers(List<? extends Number> numbers){for(Number number : numbers){System.out.println(number);}}
You can safely read from the list, but you cannot add arbitrary Number objects because the actual subtype is unknown.
Lower Bounded Wildcards
List<? super Integer>
Accepts:
- Integer
- Number
- Object
Example:
public void addNumbers(List<? super Integer> list){list.add(10);}
Lower-bounded wildcards are useful when adding elements.
PECS Principle
One of the most important interview topics.
PECS means:
Producer ExtendsConsumer Super
| Scenario | Wildcard |
|---|---|
| Reading Data | ? extends T |
| Writing Data | ? super T |
Example:
List<? extends Number> producer;List<? super Integer> consumer;
Type Erasure
Generics exist only at compile time.
Compiler:
List<String> names =new ArrayList<>();
Becomes (conceptually after compilation):
List names =new ArrayList();
The compiler inserts necessary casts where required.
Consequences:
- Generic type information is not available at runtime.
- You cannot create arrays of parameterized types.
- You cannot use
instanceofwith specific generic arguments.
Generic Collections
List<String> names =new ArrayList<>();Set<Integer> marks =new HashSet<>();Map<Integer,String> employees =new HashMap<>();
Generics make collections type-safe.
Diamond Operator
Java 7 introduced:
List<String> names =new ArrayList<>();
Instead of:
List<String> names =new ArrayList<String>();
The compiler infers the generic type.
Generic Arrays
This is invalid:
new T[10];
Reason:
Type erasure prevents creating arrays of a generic type parameter directly.
A common alternative is to use collections such as ArrayList<T>.
Real-World Example: Spring Repository
interface Repository<T>{void save(T entity);}
Every Spring Data repository uses generics to work with different entity types while maintaining type safety.
Real-World Example: Generic Service
class Response<T>{private T data;public T getData(){return data;}}
Using it:
Response<Employee> response =new Response<>();
Best Practices
Always Use Generics
Prefer:
List<String> list =new ArrayList<>();
Instead of raw collections.
Avoid Raw Types
Raw types bypass compile-time type checking and should generally be avoided.
Prefer Interfaces
List<Employee> employees =new ArrayList<>();
Rather than:
ArrayList<Employee> employees =new ArrayList<>();
Follow PECS
- Read → extends
- Write → super
Use Meaningful Type Names
Common conventions:
- T → Type
- E → Element
- K → Key
- V → Value
- R → Result
Common Mistakes
Using Raw Types
Raw collections lose type safety.
Confusing extends and super
Remember the PECS principle.
Assuming Generic Information Exists at Runtime
Type erasure removes generic type information during compilation.
Creating Generic Arrays
Prefer collections instead.
Excessive Wildcard Usage
Use wildcards only when flexibility is required. Otherwise, use concrete generic types.
Hands-on Exercise
Create a Java program that:
- Creates a generic
Box<T>class. - Creates a generic
Pair<K,V>class. - Implements a generic
Repository<T>interface. - Creates a generic method to print any object.
- Creates a
Calculator<T extends Number>class. - Demonstrates
? extendsand? super. - Applies the PECS principle with two methods.
- Uses generics with
List,Set, andMap. - Demonstrates the diamond operator.
- Explains why generic arrays are not allowed.
Summary
Java Generics provide compile-time type safety, eliminate unnecessary casting, and enable reusable, flexible APIs. They form the foundation of the Java Collections Framework, Streams API, Spring Data repositories, and many enterprise libraries. Understanding bounded type parameters, wildcards, the PECS principle, and type erasure is essential for writing modern Java applications and succeeding in technical interviews.
Key Takeaways
- Generics provide compile-time type safety.
- Generic classes, methods, and interfaces increase code reusability.
- Bounded type parameters restrict allowable types.
- ? extends is used for producers (reading).
- ? super is used for consumers (writing).
- PECS stands for Producer Extends, Consumer Super.
- Type erasure removes generic type information at runtime.
- Prefer generic collections over raw collections.
- Use the diamond operator (
<>) for cleaner code.
Professional Interview Questions
1What are Generics in Java?
Professional Answer
Generics are a language feature introduced in Java 5 that allows classes, interfaces, and methods to operate on different data types while providing compile-time type safety. They reduce the need for explicit casting, improve code readability, and promote reusable APIs.
Follow-up Questions
- Why were Generics introduced?
- Which Java version introduced Generics?
Interview Tip: Remember: Generics = Type Safety + Reusability.
2What is the difference between ? extends and ? super?
Professional Answer
? extends T defines an upper-bounded wildcard and is used when a method only needs to read or produce objects of type T or its subclasses. ? super T defines a lower-bounded wildcard and is used when a method needs to consume or add objects of type T. This follows the PECS principle: Producer Extends, Consumer Super.
Follow-up Questions
- Why can't you safely add elements to List<? extends Number>?
- When would you use List<? super Integer>?
Interview Tip: A quick memory trick: Read → extends, Write → super.
3What is Type Erasure?
Professional Answer
Type erasure is the process by which the Java compiler removes generic type information during compilation to maintain backward compatibility with pre-Java 5 code. After compilation, generic type parameters are replaced with their bounds (or Object if unbounded), and the compiler inserts necessary casts where appropriate.
Follow-up Questions
- Why can't you create new T[]?
- Is generic type information available at runtime?
- How does type erasure affect reflection?
Interview Tip: Remember: Generics exist at compile time. Type erasure happens before runtime. The JVM works with the erased types, not the generic type parameters.