Popular Searches
Popular Course Categories
Popular Courses

Introduction to Generics in Java

Introduction to Generics in Java

Generics and Type Safety

Introduction to Generics in Java

Course: Core Java Training

Topic: Introduction to Generics in Java

Course Link: Core Java Training Course

Demo Link: Register for a Course Demo

JustAcademy Website: Visit JustAcademy


1. Introduction to Generics

Generics are a feature of Java that allows classes, interfaces, and methods to work with different data types while maintaining compile-time type safety. Instead of writing separate classes or methods for integers, strings, and other object types, developers can define reusable code using type parameters.

For example, a generic class can store an Integer in one instance and a String in another instance. The compiler checks whether the values used with each instance match its declared type.

Generics are widely used in the Java Collections Framework, including classes such as ArrayList<E>, HashMap<K,V>, and HashSet<E>.

Learning Objectives

  • Understand the meaning and purpose of generics in Java.
  • Identify the limitations of non-generic code.
  • Create generic classes, interfaces, and methods.
  • Understand type parameters and type arguments.
  • Work with multiple type parameters and bounded types.
  • Learn the basics of wildcards and generic collections.
  • Recognize type erasure, raw types, and common generics errors.

2. Why Are Generics Needed?

Before generics were introduced in Java 5, collections commonly stored values as Object. Because most Java reference types inherit from Object, a collection could store different kinds of objects. However, retrieving an element often required an explicit cast, and incorrect casts could cause runtime exceptions.

Example Without Generics

import java.util.ArrayList;
import java.util.List;

public class WithoutGenericsExample {
    public static void main(String[] args) {
        List values = new ArrayList();

        values.add("Java");
        values.add(100);

        String language = (String) values.get(0);
        System.out.println(language);

        String number = (String) values.get(1);
        System.out.println(number);
    }
}

Output:

Java
Exception in thread "main" java.lang.ClassCastException

The second element is an Integer, but the program attempts to cast it to String. The compiler cannot catch this mistake when the collection is used as a raw, non-generic list.

Solution Using Generics

import java.util.ArrayList;
import java.util.List;

public class WithGenericsExample {
    public static void main(String[] args) {
        List<String> values = new ArrayList<>();

        values.add("Java");
        values.add("Generics");

        String first = values.get(0);
        System.out.println(first);

        // Compile-time error:
        // values.add(100);
    }
}

Output:

Java

Here, List<String> accepts String values only. The compiler prevents an Integer from being added, and retrieving a value does not require an explicit cast.

3. Main Advantages of Generics

Advantage Explanation
Type safety Helps prevent incompatible values from being added to a typed collection.
Compile-time checking Many type-related errors are detected by the compiler before the program runs.
Less casting Retrieved values can be assigned to their declared types without manual casts.
Code reusability One class or method can work with many reference types.
Readability Type declarations make the intended data type clear.
Maintainability Type relationships are easier to understand and enforce as programs grow.

4. Understanding Type Parameters and Type Arguments

A type parameter is a placeholder for a type in a generic declaration. A type argument is the actual type supplied when the generic class, interface, or method is used.

class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

In this example, T is a type parameter. When the class is used as Box<String>, String is the type argument.

Box<String> box = new Box<>();
box.set("Hello Java");

String message = box.get();
System.out.println(message);

Output:

Hello Java

The same generic class can be used with another reference type:

Box<Integer> numberBox = new Box<>();
numberBox.set(250);

Integer number = numberBox.get();
System.out.println(number);

Output:

250

5. Generic Type Naming Conventions

Java developers commonly use short uppercase letters for type parameters. These names are conventions, not reserved keywords.

Type Parameter Common Meaning Example
T Type Box<T>
E Element List<E>
K Key Map<K,V>
V Value Map<K,V>
N Number Calculator<N>
S, U Additional types Pair<T,U>

6. Creating a Generic Class

A generic class declares one or more type parameters after its class name. The type parameter can be used for fields, constructor parameters, method parameters, and return types.

Syntax

class ClassName<T> {
    private T data;

    public ClassName(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

Example: Generic Box Class

class Box<T> {
    private T item;

    public Box(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    public void display() {
        System.out.println("Item: " + item);
    }
}

public class GenericClassDemo {
    public static void main(String[] args) {
        Box<String> textBox = new Box<>("Programming");
        Box<Integer> numberBox = new Box<>(500);

        textBox.display();
        numberBox.display();
    }
}

Output:

Item: Programming
Item: 500

Explanation

  • Box<T> declares a generic class with one type parameter.
  • Box<String> uses String as the type argument.
  • Box<Integer> uses Integer as the type argument.
  • The compiler checks the type used when each box is created and accessed.

7. Creating a Generic Class with Multiple Type Parameters

A generic class can use more than one type parameter. This is useful when an object needs to store related values of different types, such as a key and a value or a person's name and age.

Example: Pair Class

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;
    }
}

public class PairDemo {
    public static void main(String[] args) {
        Pair<String, Integer> student =
                new Pair<>("Rahul", 85);

        System.out.println("Name: " + student.getKey());
        System.out.println("Marks: " + student.getValue());
    }
}

Output:

Name: Rahul
Marks: 85

In this example, K represents the key type and V represents the value type. The same class can also be used with combinations such as Pair<Integer, String>.

8. Creating a Generic Interface

Interfaces can also declare type parameters. A generic interface defines operations that can work with a selected type, while implementing classes provide the actual behavior.

Example

interface Container<T> {
    void add(T item);
    T get();
}

class StringContainer implements Container<String> {
    private String item;

    @Override
    public void add(String item) {
        this.item = item;
    }

    @Override
    public String get() {
        return item;
    }
}

public class GenericInterfaceDemo {
    public static void main(String[] args) {
        Container<String> container = new StringContainer();

        container.add("Java Generics");
        System.out.println(container.get());
    }
}

Output:

Java Generics

Because the implementation uses Container<String>, the add() method accepts String values and get() returns a String.

9. Creating a Generic Method

A generic method declares its own type parameter independently of whether it belongs to a generic class. The type parameter appears before the return type in the method declaration.

Syntax

public static <T> void methodName(T value) {
    // Method implementation
}

Example: Displaying Different Types

public class GenericMethodDemo {
    public static <T> void display(T value) {
        System.out.println("Value: " + value);
    }

    public static void main(String[] args) {
        display("Hello");
        display(100);
        display(25.5);
        display(true);
    }
}

Output:

Value: Hello
Value: 100
Value: 25.5
Value: true

The compiler infers the type argument from each method call. A generic method can be reused with different reference types without writing a separate method for every type.

10. Generic Methods with Multiple Parameters

A generic method can declare multiple type parameters when it needs to relate different input or output types.

public class GenericPairMethod {
    public static <A, B> void displayPair(A first, B second) {
        System.out.println("First: " + first);
        System.out.println("Second: " + second);
    }

    public static void main(String[] args) {
        displayPair("Age", 25);
        displayPair("Price", 199.99);
    }
}

Output:

First: Age
Second: 25
First: Price
Second: 199.99

11. Bounded Type Parameters

Sometimes a generic class or method should accept only types that satisfy a particular requirement. A bounded type parameter restricts the allowed types.

Use the extends keyword to declare an upper bound. The bound may be a class or an interface. When the bound is an interface, extends is still used in the type parameter declaration.

Syntax

<T extends Number>

This declaration allows Number and its subclasses, such as Integer, Double, and Float.

Example: Adding Numeric Values

public class BoundedGenericDemo {
    public static <T extends Number> double add(
            T first, T second) {
        return first.doubleValue() + second.doubleValue();
    }

    public static void main(String[] args) {
        System.out.println(add(10, 20));
        System.out.println(add(12.5, 7.5));

        // Compile-time error:
        // add("Hello", "Java");
    }
}

Output:

30.0
20.0

The bound allows the method to call doubleValue(), which is defined by Number. String does not extend Number, so it cannot be passed to this method.

12. Bounded Types with Classes and Interfaces

A type parameter can have both a class bound and one or more interface bounds. If a class bound is present, it must appear first.

<T extends Number & Comparable<T>>

This example restricts T to types that extend Number and implement Comparable<T>. Multiple interface bounds can also be specified, separated by the & symbol.

Example

public class BoundedTypeInfo {
    public static <T extends Number & Comparable<T>>
    void printValue(T value) {
        System.out.println(value.doubleValue());
        System.out.println(value.compareTo(value));
    }
}

Bounds are useful when a generic method needs to call methods declared by a particular superclass or interface.

13. Understanding Wildcards in Generics

A wildcard is represented by the question mark (?). It represents an unknown type and makes generic methods more flexible when they need to accept collections with different element types.

For example, List<String> and List<Integer> are both compatible with a parameter declared as List<?>.

Example: Printing Any List

import java.util.Arrays;
import java.util.List;

public class WildcardDemo {
    public static void printList(List<?> values) {
        for (Object value : values) {
            System.out.println(value);
        }
    }

    public static void main(String[] args) {
        List<String> names = Arrays.asList("Amit", "Neha");
        List<Integer> numbers = Arrays.asList(10, 20, 30);

        printList(names);
        printList(numbers);
    }
}

Output:

Amit
Neha
10
20
30

A List<?> allows elements to be read as Object, but arbitrary non-null values cannot be added because the actual element type is unknown.

14. Upper Bounded Wildcards

An upper bounded wildcard uses ? extends Type. It accepts the specified type or one of its subtypes. It is useful when a method primarily needs to read values from a collection.

Example

import java.util.List;

public class UpperBoundDemo {
    public static double total(List<? extends Number> numbers) {
        double sum = 0;

        for (Number number : numbers) {
            sum += number.doubleValue();
        }

        return sum;
    }

    public static void main(String[] args) {
        System.out.println(total(List.of(10, 20, 30)));
        System.out.println(total(List.of(1.5, 2.5, 3.0)));
    }
}

Output:

60.0
7.0

The method accepts lists of Integer, Double, or other Number subclasses. You generally cannot add a non-null Number value to a collection declared with ? extends Number, because the exact element type is unknown.

15. Lower Bounded Wildcards

A lower bounded wildcard uses ? super Type. It accepts the specified type or one of its supertypes. It is useful when a method needs to add values of a particular type to a collection.

Example

import java.util.List;

public class LowerBoundDemo {
    public static void addNumbers(List<? super Integer> values) {
        values.add(10);
        values.add(20);
        values.add(30);
    }

    public static void main(String[] args) {
        List<Number> numbers =
                new java.util.ArrayList<>();

        addNumbers(numbers);
        System.out.println(numbers);
    }
}

Output:

[10, 20, 30]

A parameter of type List<? super Integer> can accept lists such as List<Integer>, List<Number>, and List<Object>. Integer values can safely be added. When reading elements from this list, the compiler can guarantee only that they are Objects.

16. The PECS Rule

PECS is a useful guideline for deciding between upper and lower bounded wildcards. It stands for Producer Extends, Consumer Super.

Rule Wildcard Typical Purpose
Producer Extends ? extends T Read or produce values of type T from a source.
Consumer Super ? super T Write or consume values of type T into a destination.

Use ? extends T when a parameter supplies values for processing, and ? super T when a parameter receives values of type T. If a structure needs to both read and write a specific type, an ordinary type parameter is often more suitable.

17. Generics and the Java Collections Framework

The Collections Framework uses generics to provide type-safe data structures. When declaring a collection, specify the type of elements it is intended to store.

Example: Generic ArrayList

import java.util.ArrayList;
import java.util.List;

public class GenericListDemo {
    public static void main(String[] args) {
        List<String> languages = new ArrayList<>();

        languages.add("Java");
        languages.add("Python");
        languages.add("JavaScript");

        for (String language : languages) {
            System.out.println(language);
        }
    }
}

Output:

Java
Python
JavaScript

Example: Generic HashMap

import java.util.HashMap;
import java.util.Map;

public class GenericMapDemo {
    public static void main(String[] args) {
        Map<String, Integer> scores = new HashMap<>();

        scores.put("Amit", 85);
        scores.put("Neha", 92);

        int amitScore = scores.get("Amit");
        System.out.println("Amit's score: " + amitScore);
    }
}

Output:

Amit's score: 85

Here, String is the key type and Integer is the value type. Generics help prevent incompatible key and value types from being inserted into the map.

18. Generic Types and Inheritance

Java's generic types do not follow the same subtype relationship as their type arguments. For example, Integer is a subtype of Number, but List<Integer> is not a subtype of List<Number>.

List<Integer> integers = new ArrayList<>();

// Not allowed:
// List<Number> numbers = integers;

If this assignment were allowed, code could add a Double to a list that was originally intended to contain only Integers. Java prevents this to preserve type safety.

Use a wildcard when you need a compatible view of lists with related element types:

List<Integer> integers = new ArrayList<>();
List<? extends Number> numbers = integers;

This allows the reference to read Number values from the list, but it does not allow arbitrary Number values to be added.

19. Type Erasure in Java

Java implements generics primarily through a mechanism called type erasure. During compilation, the compiler uses generic type information to check type safety and then removes or translates most of that type information in the generated bytecode.

For example, an unbounded type parameter may be represented as Object after erasure, while a bounded type parameter is represented using its first bound. The compiler may insert casts where required to preserve the source program's behavior.

Important Points About Type Erasure

  • Generic type checking happens primarily at compile time.
  • Different parameterizations such as Box<String> and Box<Integer> generally share the same runtime class.
  • You cannot normally create an object directly using new T().
  • You cannot use a type parameter directly in an ordinary class instance check such as value instanceof T.
  • Generic type arguments are generally not available as ordinary runtime type information.

20. Limitations of Generics

Although generics improve type safety, Java places some restrictions on their use because of type erasure and compatibility with existing code.

Restriction Explanation
Primitive type arguments Types such as int and double cannot be used directly as type arguments. Use wrapper classes such as Integer and Double.
Direct creation of T new T() is not allowed because the actual type is not available in the required form at runtime.
Generic arrays Creating arrays of parameterized types, such as new List<String>[10], is not allowed.
Static fields using class type parameters A static field cannot use a class's type parameter as though it belonged to a particular instance.
Parameterized instanceof checks Checks such as value instanceof List<String> are not permitted because the parameterized type is not reified at runtime.

21. Raw Types and Unchecked Warnings

A raw type is a generic class or interface used without specifying its type argument. For example, List is the raw form of List<E>.

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

Raw types can bypass generic type checks and may produce unchecked warnings or runtime type errors. They mainly exist for compatibility with older Java code.

Prefer parameterized types:

List<String> safeList = new ArrayList<>();
safeList.add("Java");

// Compile-time error:
// safeList.add(100);

Use raw types only when necessary for legacy interoperability, and handle unchecked operations carefully.

22. Common Generics Errors and Solutions

Error Reason Solution
Adding an incompatible type The value does not match the declared type argument. Use the correct type or redesign the collection's type.
Assigning List<Integer> to List<Number> Generic types are invariant. Use an appropriate wildcard, such as List<? extends Number>, when suitable.
Using a primitive type as a type argument Generics require reference types. Use wrapper classes such as Integer or Double.
Creating a generic array directly Parameterized array creation is restricted. Prefer a collection or use a carefully designed alternative when an array is essential.
Using a raw collection Type checking is weakened and unchecked warnings may occur. Specify the type argument wherever possible.
Trying to instantiate T directly The type parameter is not available for direct object construction. Pass a factory, constructor reference, or other suitable creation mechanism.

23. Best Practices for Using Generics

  • Specify type arguments for collections rather than using raw types.
  • Use meaningful generic class and method names that explain their purpose.
  • Follow common type parameter conventions such as T, E, K, and V.
  • Prefer generic methods when an operation can work safely with multiple types.
  • Use bounded type parameters when the implementation needs methods from a specific superclass or interface.
  • Use wildcards when they make method parameters more flexible without losing type safety.
  • Apply the PECS guideline to producer and consumer parameters.
  • Avoid unnecessary unchecked casts and suppress warnings only when the operation is demonstrably safe.
  • Remember that generic type arguments do not generally provide runtime type information.
  • Use wrapper classes such as Integer and Double when working with generic collections of numeric values.

24. Practical Example: Generic Storage System

This example creates a reusable storage class that can store and retrieve values of different types while maintaining type safety.

class Storage<T> {
    private T value;

    public void store(T value) {
        this.value = value;
    }

    public T retrieve() {
        return value;
    }
}

public class StorageSystemDemo {
    public static void main(String[] args) {
        Storage<String> nameStorage = new Storage<>();
        nameStorage.store("Rahul");

        Storage<Integer> ageStorage = new Storage<>();
        ageStorage.store(24);

        Storage<Double> priceStorage = new Storage<>();
        priceStorage.store(499.99);

        System.out.println("Name: " + nameStorage.retrieve());
        System.out.println("Age: " + ageStorage.retrieve());
        System.out.println("Price: " + priceStorage.retrieve());
    }
}

Output:

Name: Rahul
Age: 24
Price: 499.99

How the Program Works

  1. The Storage class declares a type parameter named T.
  2. The store method accepts a value of the chosen type.
  3. The retrieve method returns the same declared type.
  4. Separate Storage instances hold String, Integer, and Double values.
  5. The compiler checks each instance's type usage before the program runs.

25. Practice Exercises

  1. Create a generic class called Box<T> that stores one value and provides getter and setter methods.
  2. Create a generic Pair class that stores a person's name and age.
  3. Write a generic method that prints an array of any reference type.
  4. Create a generic interface called Repository<T> with methods to save and retrieve a value.
  5. Write a bounded generic method that accepts Number values and calculates their sum.
  6. Create a method using List<?> that prints elements from lists of different types.
  7. Write a method that accepts List<? extends Number> and calculates the average.
  8. Write a method that accepts List<? super Integer> and adds several integer values.
  9. Demonstrate why List<Integer> cannot be assigned to List<Number>.
  10. Compare a raw ArrayList with a type-safe ArrayList<String>.

26. Mini Project: Generic Inventory Container

Project Objective: Build a small inventory system that demonstrates how generics support reusable and type-safe storage.

Project Requirements

  • Create a generic class named InventoryItem<T>.
  • Store an item code and an item value using suitable types.
  • Create instances for String product names, Integer stock quantities, and Double prices.
  • Create a generic method that displays item information.
  • Use a bounded type parameter in a separate method for numeric inventory values.
  • Use a wildcard method to display values from different typed collections.
  • Test valid values and observe the compiler errors caused by incompatible values.

Expected Learning Outcome

After completing the project, learners should be able to apply generic classes, methods, bounds, and wildcards to a practical Java program.

27. Interview Questions and Answers

Q1. What are generics in Java?

Answer: Generics allow classes, interfaces, and methods to operate on parameterized types, improving compile-time type safety and code reuse.

Q2. Why were generics introduced?

Answer: They were introduced to strengthen type checking, reduce explicit casts, and make reusable data structures and algorithms safer.

Q3. What is a type parameter?

Answer: A type parameter is a placeholder such as T or E in a generic declaration. It is replaced by a concrete type argument when the generic type is used.

Q4. What is the difference between a type parameter and a type argument?

Answer: T in Box<T> is a type parameter. String in Box<String> is the type argument.

Q5. Can a generic class have multiple type parameters?

Answer: Yes. For example, Pair<K,V> uses two type parameters to represent different types.

Q6. What is a generic method?

Answer: A generic method declares its own type parameter, allowing the method to work with different types while preserving type checking.

Q7. What is a bounded type parameter?

Answer: A bounded type parameter restricts the allowed types. For example, <T extends Number> accepts Number and its subclasses.

Q8. What is a wildcard in Java generics?

Answer: The ? symbol represents an unknown type. It can be used in declarations such as List<?> to accept lists with different element types.

Q9. What is the difference between ? extends T and ? super T?

Answer: ? extends T is an upper bounded wildcard commonly used for reading values. ? super T is a lower bounded wildcard commonly used for adding values of type T.

Q10. What is type erasure?

Answer: Type erasure is the compilation process that removes or translates most generic type information in bytecode while preserving type checks through compiler-generated mechanisms.

Q11. Can primitive types be used as generic type arguments?

Answer: No. Use wrapper classes such as Integer, Double, and Boolean instead of int, double, and boolean.

Q12. Why is List<Integer> not a subtype of List<Number>?

Answer: Generic types are invariant. Allowing the assignment could permit a value such as Double to be inserted into a list intended to contain only Integer values.

Q13. What is a raw type?

Answer: A raw type is a generic type used without its type argument, such as List instead of List<String>. Raw types weaken compile-time type safety.

Q14. Can we instantiate a type parameter using new T()?

Answer: No. Java does not allow direct object creation with new T(). A factory or another explicit object-creation strategy can be used instead.

Q15. Where are generics commonly used in Java?

Answer: Generics are used in collections, reusable data structures, repository classes, utility methods, APIs, and algorithms that operate on different reference types.

28. Quick Revision Checklist

  • Generics provide compile-time type safety and support reusable code.
  • ClassName<T> declares a generic class.
  • <T> returnType methodName() declares a generic method.
  • K and V commonly represent map keys and values.
  • <T extends Number> restricts T to Number and its subclasses.
  • List<?> accepts a list with an unknown element type.
  • List<? extends T> supports reading values as T or a compatible supertype.
  • List<? super T> supports adding T values.
  • PECS means Producer Extends, Consumer Super.
  • Type erasure explains many restrictions on generics at runtime.
  • Prefer parameterized types over raw types.

29. Conclusion

Generics are an essential feature of Core Java that helps developers create reusable, readable, and type-safe code. By using type parameters, generic classes, generic methods, bounded types, and wildcards, developers can design flexible APIs without giving up compile-time checking.

Understanding generics is especially important when working with the Java Collections Framework, building reusable components, and preparing for Java programming interviews. Practice the examples and exercises to develop a strong foundation in this topic.


Learn More with JustAcademy

Explore the Core Java Training Course to learn more about Java fundamentals, object-oriented programming, collections, generics, exception handling, and other important Java concepts.

Interested in a course demonstration? Register for a Course Demo.

Visit the JustAcademy official website to explore available training programs.

Previous Next
whatsapp