Generics in Java

Mohit  UpadhyayMohit Upadhyay
6 min read

On Day 18, the focus is on understanding Generics in Java, a powerful feature introduced in Java 5.

Generics enable developers to write flexible and reusable code while maintaining strong type checking at compile time. Generics allow classes, methods, and interfaces to operate on any data type, providing compile-time safety and reducing the need for casting objects.


1. What are Generics?

Generics provide a mechanism for defining classes, methods, and interfaces with type parameters. This means that the same code can work with different data types while ensuring type safety. Generics help prevent ClassCastException and make the code more readable and maintainable.

Before generics, developers had to use Object types and cast them to the required type, which could lead to runtime errors. With generics, the type is specified at the time of object creation, and the compiler enforces type safety.


2. Why Use Generics?

A. Type Safety

Generics enforce type safety by ensuring that only the specified type of object is passed to a collection or method. This eliminates potential runtime errors that can occur due to incorrect type casting.

Example Without Generics:
import java.util.ArrayList;

public class WithoutGenerics {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();  // Raw type, no type safety
        list.add("Hello");
        list.add(10);  // Allowed, but problematic

        String s = (String) list.get(1);  // Causes ClassCastException at runtime
    }
}
Example With Generics:
import java.util.ArrayList;

public class WithGenerics {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();  // Type-safe
        list.add("Hello");
        // list.add(10);  // Compile-time error, ensures type safety

        String s = list.get(0);  // No casting needed
        System.out.println(s);
    }
}

By specifying ArrayList<String>, we ensure that only String objects can be added, and there's no need for casting when retrieving elements.


B. Reusability

Generics allow code to be reused for different data types without duplicating code. Instead of writing separate methods or classes for different data types, you can use a single generic method or class.

Example Without Generics:
public class PrintInteger {
    public void print(Integer value) {
        System.out.println(value);
    }
}

public class PrintString {
    public void print(String value) {
        System.out.println(value);
    }
}
Example With Generics:
public class Print<T> {  // T is the type parameter
    public void print(T value) {
        System.out.println(value);
    }
}

public class Main {
    public static void main(String[] args) {
        Print<Integer> integerPrinter = new Print<>();
        integerPrinter.print(10);

        Print<String> stringPrinter = new Print<>();
        stringPrinter.print("Hello");
    }
}

Here, the Print class can be used for any type, making it reusable and more flexible.


3. Generic Classes

A generic class allows you to define a class with type parameters. These type parameters can be used throughout the class as placeholders for the actual types that are passed when the class is instantiated.

Syntax:
class ClassName<T> {
    // T is the type parameter
}
Example:
class Box<T> {
    private T value;

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

    public T getValue() {
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>();
        intBox.setValue(123);
        System.out.println("Integer Value: " + intBox.getValue());

        Box<String> strBox = new Box<>();
        strBox.setValue("Hello");
        System.out.println("String Value: " + strBox.getValue());
    }
}

In this example, the Box class can hold any type of object (Integer, String, etc.), providing flexibility while maintaining type safety.


4. Generic Methods

A generic method allows you to define a method with type parameters. These type parameters can be used within the method to operate on various types.

Syntax:
public <T> void methodName(T parameter) {
    // T is the type parameter
}
Example:
public class GenericMethodExample {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3};
        String[] strArray = {"Hello", "World"};

        printArray(intArray);  // Calls the generic method with Integer array
        printArray(strArray);  // Calls the generic method with String array
    }
}

The printArray method is generic and can work with any type of array (Integer, String, etc.).


5. Bounded Type Parameters

Sometimes, you may want to restrict the types that can be passed to a generic method or class. This can be done using bounded type parameters.

Example:
public class BoundedTypeExample {
    // T must be a subclass of Number
    public static <T extends Number> void printNumber(T number) {
        System.out.println("Number: " + number);
    }

    public static void main(String[] args) {
        printNumber(10);        // Integer
        printNumber(10.5);      // Double
        // printNumber("Hello");  // Compile-time error, not a subclass of Number
    }
}

In this example, the generic method printNumber only accepts types that are subclasses of Number, such as Integer, Double, or Float.


6. Wildcards in Generics

Java also supports wildcards in generics, which are represented by the question mark (?). Wildcards provide more flexibility when working with generic types, especially when you don’t know the exact type of a collection.

Types of Wildcards:

A. Unbounded Wildcard (<?>)

An unbounded wildcard allows any type of object.

public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}
B. Upper Bounded Wildcard (<? extends T>)

An upper-bounded wildcard restricts the types to subclasses of a specified type.

public static void printNumbers(List<? extends Number> list) {
    for (Number number : list) {
        System.out.println(number);
    }
}
C. Lower Bounded Wildcard (<? super T>)

A lower-bounded wildcard restricts the types to superclasses of a specified type.

public static void addNumber(List<? super Integer> list) {
    list.add(10);
}

7. Generics and Inheritance

Generics in Java do not follow inheritance in the same way as regular types. For example, if Integer is a subclass of Number, List<Integer> is not a subclass of List<Number>. This behavior can sometimes lead to confusion but is crucial for understanding type safety in generics.

Example:
List<Integer> intList = new ArrayList<>();
// List<Number> numList = intList;  // Compile-time error

You can use wildcards to address such situations.


8. Generics and Type Erasure

Java uses a mechanism called type erasure to implement generics. This means that generic type information is erased at runtime, and the generic type parameters are replaced with their bounds or Object if no bounds are specified.

For example:

  • List<String> becomes List at runtime.

  • T becomes Object or its upper bound.

Type erasure ensures backward compatibility with code written before Java introduced generics.


9. Summary

By the end of Day 18, we will have a solid understanding of:

  • The importance of Generics in Java for writing reusable, type-safe code.

  • How to create generic classes and methods.

  • The use of bounded type parameters and wildcards to enhance flexibility.

  • Best practices for working with generics and avoiding common pitfalls.

Generics are essential for writing flexible and maintainable Java code, especially when working with collections and ensuring type safety across the application. Stay tuned! for further updates related to Java.

10
Subscribe to my newsletter

Read articles from Mohit Upadhyay directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mohit  Upadhyay
Mohit Upadhyay