Generics in Java: A Complete Guide

Introduction

Generics are a powerful feature introduced in Java 5 that enable type-safe programming by allowing classes, interfaces, and methods to operate on objects of various types while providing compile-time type checking. Generics eliminate the need for explicit casting and prevent ClassCastException at runtime by ensuring that only the correct types of objects are stored and retrieved. They are widely used in the Java Collections Framework (e.g., ArrayList<String>, HashMap<Integer, String>) and are essential for writing reusable, robust, and maintainable code.


1. Why Use Generics?

Problem Without Generics

// Raw ArrayList (pre-Java 5 style)
List list = new ArrayList();
list.add("Hello");
list.add(123); // Accidentally added an Integer
String s = (String) list.get(1); // Runtime ClassCastException!

Solution With Generics

List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // Compile-time error!
String s = list.get(0); // No cast needed

Benefits of Generics

  • Type safety: Catches errors at compile time.
  • No explicit casting: Cleaner, more readable code.
  • Code reuse: Write algorithms that work with multiple types.
  • Better documentation: Type parameters clarify intended usage.

2. Generic Classes

A generic class declares one or more type parameters in angle brackets (<>).

Syntax

class ClassName<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}

Example: Generic Box

public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// Usage
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String s = stringBox.get(); // No cast
Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer n = intBox.get();

Note: Type parameters are compile-time only—erased at runtime (type erasure).


3. Generic Methods

Methods can also be generic, independent of the class.

Syntax

public <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}

Example

public class Util {
public static <T> boolean contains(T[] array, T item) {
for (T element : array) {
if (element.equals(item)) {
return true;
}
}
return false;
}
}
// Usage
String[] names = {"Alice", "Bob", "Charlie"};
boolean found = Util.contains(names, "Bob"); // true

Note: The type T is inferred from the arguments—no need to specify it explicitly.


4. Bounded Type Parameters

Restrict the types that can be used as type arguments.

A. Upper Bounded Wildcards (extends)

// Accepts Number or its subclasses (Integer, Double, etc.)
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
// Usage
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);
System.out.println(sum(ints));    // OK
System.out.println(sum(doubles)); // OK

B. Lower Bounded Wildcards (super)

// Accepts String or its superclasses (Object, CharSequence, etc.)
public static void addStrings(List<? super String> list) {
list.add("Hello");
list.add("World");
}

PECS Principle (Producer-Extends, Consumer-Super)

  • Producer (provides data): Use ? extends T
  • Consumer (accepts data): Use ? super T

5. Wildcards

Wildcards (?) represent an unknown type.

Types of Wildcards

WildcardMeaningExample
?Unbounded wildcardList<?> — list of unknown type
? extends TUpper boundedList<? extends Number>
? super TLower boundedList<? super Integer>

Example: Unbounded Wildcard

public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}

Use Case: When the method only uses functionality from Object (e.g., toString()).


6. Type Erasure

Generics are implemented using type erasure:

  • Type parameters are removed during compilation.
  • Bytecode contains only raw types (e.g., List instead of List<String>).
  • Casts are inserted automatically where needed.

Consequences

  • Cannot create instances of type parameters:
  T elem = new T(); // ❌ Compilation error
  • Cannot use instanceof with generic types:
  if (list instanceof List<String>) { } // ❌ Invalid
  • Array creation not allowed:
  T[] arr = new T[10]; // ❌ Invalid

Workaround: Pass a Class<T> object for reflection:

T[] arr = (T[]) Array.newInstance(clazz, size);

7. Common Generic Types in Java Collections

CollectionGeneric FormDescription
ListList<E>Ordered collection (e.g., ArrayList<String>)
SetSet<E>Unique elements (e.g., HashSet<Integer>)
MapMap<K, V>Key-value pairs (e.g., HashMap<String, Integer>)
QueueQueue<E>FIFO structure (e.g., LinkedList<String>)

8. Best Practices

  • Use generics everywhere possible—especially with collections.
  • Prefer bounded wildcards for flexible APIs (? extends T, ? super T).
  • Don’t use raw types (e.g., List instead of List<String>)—they bypass type safety.
  • Use meaningful type parameter names:
  • E – Element (e.g., List<E>)
  • K – Key (e.g., Map<K, V>)
  • V – Value
  • T – Type
  • S, U, V – Additional types
  • Avoid unchecked warnings—they indicate potential runtime errors.

9. Common Mistakes

  • Assuming List<String> is a subtype of List<Object>:
  List<Object> list = new ArrayList<String>(); // ❌ Compilation error

Fix: Use wildcards: List<? extends Object> list = new ArrayList<String>();

  • Trying to add to a list with upper-bounded wildcard:
  List<? extends Number> list = new ArrayList<Integer>();
list.add(42); // ❌ Not allowed (could be List<Double>)
  • Ignoring compiler warnings about unchecked operations.

10. Practical Example: Generic Repository

public class Repository<T> {
private List<T> items = new ArrayList<>();
public void save(T item) {
items.add(item);
}
public T findById(int index) {
return items.get(index);
}
public List<T> findAll() {
return new ArrayList<>(items);
}
}
// Usage
Repository<User> userRepo = new Repository<>();
userRepo.save(new User("Alice"));
User u = userRepo.findById(0);

Conclusion

Generics are a cornerstone of modern Java programming, enabling type-safe, reusable, and maintainable code. By catching type errors at compile time and eliminating the need for casts, they significantly reduce runtime failures and improve code clarity. Understanding how to define generic classes and methods, use bounded wildcards, and apply the PECS principle allows developers to write flexible and robust APIs. While type erasure imposes some limitations, these are outweighed by the safety and expressiveness generics provide. Whether working with collections, building frameworks, or designing domain models, mastering generics is essential for professional Java development. Always prefer generics over raw types—they are not just a convenience, but a critical tool for correctness.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper