Java Tutorial 0/145 lessons ~6 min read Lesson 31

    Generics

    java generics type safety wildcard extends super PECS type erasure bounded generic class method diamond operator

    Course progress0%
    Focus
    26 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    In the previous lessons, you learned about:

    • Collections Framework
    • Streams API
    • Lambda Expressions
    • Multithreading

    Consider the following code:

    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:

    code
    String language = (String) list.get(1);

    Output:

    code
    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:

    code
    ArrayList list =
    new ArrayList();

    Write:

    code
    List<String> list =
    new ArrayList<>();

    Now only String objects can be added.

    Why Generics?

    Without Generics:

    code
    List list =
    new ArrayList();
    list.add("Java");
    list.add(100);

    Later:

    code
    String text =
    (String) list.get(1);

    Runtime Error.

    With Generics:

    code
    List<String> list =
    new ArrayList<>();
    list.add("Java");
    // Compilation Error
    list.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:

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

    Using it:

    code
    Box<String> box =
    new Box<>();
    box.set("Java");
    System.out.println(box.get());

    Output:

    code
    Java

    Generic Class with Different Types

    code
    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.

    code
    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:

    code
    Pair<Integer,String> employee =
    new Pair<>(101,"Rahul");

    Generic Methods

    Generics can also be applied to methods.

    code
    public static <T> void print(T value){
    System.out.println(value);
    }

    Calling:

    code
    print("Java");
    print(100);
    print(true);

    Output:

    code
    Java
    100
    true

    Generic Interfaces

    Example:

    code
    interface Repository<T>{
    void save(T entity);
    T findById(int id);
    }

    Implementation:

    code
    class EmployeeRepository
    implements Repository<Employee>{
    @Override
    public void save(Employee entity){
    }
    @Override
    public Employee findById(int id){
    return new Employee();
    }
    }

    Bounded Type Parameters

    Sometimes we want to restrict generic types.

    Example:

    code
    class Calculator<T extends Number>{
    public double square(T value){
    return value.doubleValue() *
    value.doubleValue();
    }
    }

    Allowed:

    code
    Calculator<Integer> c1 =
    new Calculator<>();
    Calculator<Double> c2 =
    new Calculator<>();

    Not Allowed:

    code
    Calculator<String> c3 =
    new Calculator<>();

    Compilation Error.

    Multiple Bounds

    code
    <T extends Number & Comparable<T>>

    The type must:

    • Extend Number
    • Implement Comparable

    Wildcards

    Wildcard:

    code
    ?

    Represents an unknown type.

    Example:

    code
    List<?> list;

    Useful when the exact type is not important.

    Upper Bounded Wildcards

    code
    List<? extends Number>

    Accepts:

    • Integer
    • Double
    • Float
    • Long

    Example:

    code
    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

    code
    List<? super Integer>

    Accepts:

    • Integer
    • Number
    • Object

    Example:

    code
    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:

    code
    Producer Extends
    Consumer Super
    ScenarioWildcard
    Reading Data? extends T
    Writing Data? super T

    Example:

    code
    List<? extends Number> producer;
    List<? super Integer> consumer;

    Type Erasure

    Generics exist only at compile time.

    Compiler:

    code
    List<String> names =
    new ArrayList<>();

    Becomes (conceptually after compilation):

    code
    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 instanceof with specific generic arguments.

    Generic Collections

    code
    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:

    code
    List<String> names =
    new ArrayList<>();

    Instead of:

    code
    List<String> names =
    new ArrayList<String>();

    The compiler infers the generic type.

    Generic Arrays

    This is invalid:

    code
    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

    code
    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

    code
    class Response<T>{
    private T data;
    public T getData(){
    return data;
    }
    }

    Using it:

    code
    Response<Employee> response =
    new Response<>();

    Best Practices

    Always Use Generics

    Prefer:

    code
    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

    code
    List<Employee> employees =
    new ArrayList<>();

    Rather than:

    code
    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:

    1. Creates a generic Box<T> class.
    2. Creates a generic Pair<K,V> class.
    3. Implements a generic Repository<T> interface.
    4. Creates a generic method to print any object.
    5. Creates a Calculator<T extends Number> class.
    6. Demonstrates ? extends and ? super.
    7. Applies the PECS principle with two methods.
    8. Uses generics with List, Set, and Map.
    9. Demonstrates the diamond operator.
    10. 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.

    Ready to mark this lesson complete?Track your journey across the entire course.