Skip to main content

Java Generics

Introduction

Java Generics is a powerful feature introduced in Java 5 (JDK 1.5) that allows you to create classes, interfaces, and methods that operate on a type parameter. Instead of working with specific types like String or Integer, generics let you write code that can work with any type while maintaining type safety.

Before generics, you had to cast objects when retrieving them from collections, which was error-prone and could lead to runtime errors. Generics bring type safety to compile-time, making your code more robust and readable.

In this tutorial, you'll learn how generics work, how to use them in your own code, and why they're essential for modern Java development, especially when working with Spring Framework.

Why Use Generics?

Generics provide several key benefits:

  1. Type Safety: Catch type errors at compile-time rather than runtime
  2. Elimination of Casting: No need for explicit casting when retrieving elements
  3. Code Reusability: Write algorithms that work with different types
  4. Better API Design: Create more flexible and type-safe libraries

Basic Syntax

Generic types are declared using angle brackets <> with a type parameter, typically a single uppercase letter. Common conventions include:

  • T for Type
  • E for Element
  • K for Key
  • V for Value
  • N for Number

Let's start with a simple example:

java
// Without generics
List myList = new ArrayList();
myList.add("Hello");
myList.add(123); // This is allowed, but not type-safe
String item = (String) myList.get(0); // Requires explicit casting
String problem = (String) myList.get(1); // Runtime error: ClassCastException

// With generics
List<String> myGenericList = new ArrayList<>();
myGenericList.add("Hello");
// myGenericList.add(123); // Compile-time error - type safety!
String itemSafe = myGenericList.get(0); // No casting needed

Creating Generic Classes

You can create your own generic classes to make your code more reusable. Here's a simple container class that can hold any type of object:

java
public class Container<T> {
private T value;

public Container(T value) {
this.value = value;
}

public T getValue() {
return value;
}

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

Now, you can use this container for any type:

java
// Using with String
Container<String> stringContainer = new Container<>("Hello Generics");
String strValue = stringContainer.getValue(); // No casting needed
System.out.println(strValue); // Output: Hello Generics

// Using with Integer
Container<Integer> intContainer = new Container<>(42);
int intValue = intContainer.getValue(); // Auto-unboxing
System.out.println(intValue); // Output: 42

// Using with custom class
class Person {
private String name;
public Person(String name) { this.name = name; }
public String getName() { return name; }
}

Container<Person> personContainer = new Container<>(new Person("John"));
Person person = personContainer.getValue();
System.out.println(person.getName()); // Output: John

Generic Methods

You can also create generic methods that operate on parameterized types:

java
public class Utilities {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}

public static <T> T findFirst(T[] array) {
if (array.length > 0) {
return array[0];
}
return null;
}
}

And use them like this:

java
String[] strings = {"Hello", "Generics", "World"};
Integer[] numbers = {1, 2, 3, 4, 5};

Utilities.printArray(strings); // Output: Hello Generics World
Utilities.printArray(numbers); // Output: 1 2 3 4 5

String firstString = Utilities.<String>findFirst(strings);
Integer firstNumber = Utilities.<Integer>findFirst(numbers);

System.out.println("First string: " + firstString); // Output: First string: Hello
System.out.println("First number: " + firstNumber); // Output: First number: 1

Note that in many cases, you can omit the explicit type as Java will infer it:

java
// Type inference in action - no need to specify the type
String firstString = Utilities.findFirst(strings);
Integer firstNumber = Utilities.findFirst(numbers);

Bounded Type Parameters

Sometimes you need to restrict the types that can be used with your generic class or method. You can do this with bounded type parameters:

java
// T can be any type that is a Number or a subclass of Number
public class MathBox<T extends Number> {
private T value;

public MathBox(T value) {
this.value = value;
}

public double sqrt() {
return Math.sqrt(value.doubleValue()); // Allowed because T extends Number
}

public T getValue() {
return value;
}
}

Now you can use this with any numeric type:

java
MathBox<Integer> integerMathBox = new MathBox<>(16);
System.out.println(integerMathBox.sqrt()); // Output: 4.0

MathBox<Double> doubleMathBox = new MathBox<>(25.0);
System.out.println(doubleMathBox.sqrt()); // Output: 5.0

// This won't compile:
// MathBox<String> stringMathBox = new MathBox<>("Hello"); // Error!

Multiple Bounds

You can also specify multiple bounds using the & operator:

java
interface Displayable {
void display();
}

// T must be both a Number and implement Displayable
public class DisplayableMathBox<T extends Number & Displayable> {
private T value;

public DisplayableMathBox(T value) {
this.value = value;
}

public void showAndCalculate() {
value.display(); // From Displayable interface
System.out.println("Square root: " + Math.sqrt(value.doubleValue())); // From Number class
}
}

Wildcard Types

Wildcards represent unknown types and come in three forms:

  1. Unbounded Wildcard: <?>
  2. Upper Bounded Wildcard: <? extends Type>
  3. Lower Bounded Wildcard: <? super Type>

Unbounded Wildcard

Use when you want to accept any type:

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

Upper Bounded Wildcard

Use when you need methods from a specific type or its subtypes:

java
// Accepts a list of Number or any subclass (Integer, Double, etc.)
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}

Usage example:

java
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);

System.out.println(sumOfList(integers)); // Output: 6.0
System.out.println(sumOfList(doubles)); // Output: 6.6

Lower Bounded Wildcard

Use when you want to add elements to a collection:

java
// Can add Integer or any supertype of Integer (Number, Object)
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}

Usage example:

java
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

addNumbers(integers);
addNumbers(numbers);
addNumbers(objects);

System.out.println(integers); // Output: [1, 2, 3, 4, 5]
System.out.println(numbers); // Output: [1, 2, 3, 4, 5]
System.out.println(objects); // Output: [1, 2, 3, 4, 5]

Real-World Example: Generic Repository Pattern

A common use of generics in Spring applications is the Repository pattern. Here's a simplified example:

java
// Generic Entity
public abstract class BaseEntity {
protected Long id;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}
}

// Specific Entity
public class User extends BaseEntity {
private String username;

public User(String username) {
this.username = username;
}

public String getUsername() {
return username;
}
}

// Generic Repository Interface
public interface Repository<T extends BaseEntity> {
T findById(Long id);
List<T> findAll();
void save(T entity);
void delete(T entity);
}

// Implementation for User
public class UserRepository implements Repository<User> {
private Map<Long, User> users = new HashMap<>();
private Long nextId = 1L;

@Override
public User findById(Long id) {
return users.get(id);
}

@Override
public List<User> findAll() {
return new ArrayList<>(users.values());
}

@Override
public void save(User entity) {
if (entity.getId() == null) {
entity.setId(nextId++);
}
users.put(entity.getId(), entity);
}

@Override
public void delete(User entity) {
users.remove(entity.getId());
}
}

Using the repository:

java
UserRepository userRepo = new UserRepository();

User john = new User("john_doe");
User jane = new User("jane_doe");

userRepo.save(john);
userRepo.save(jane);

System.out.println("Users:");
for (User user : userRepo.findAll()) {
System.out.println("ID: " + user.getId() + ", Username: " + user.getUsername());
}

User foundUser = userRepo.findById(1L);
System.out.println("Found user: " + foundUser.getUsername());

userRepo.delete(john);
System.out.println("After deletion, users count: " + userRepo.findAll().size());

Output:

Users:
ID: 1, Username: john_doe
ID: 2, Username: jane_doe
Found user: john_doe
After deletion, users count: 1

Type Erasure

It's important to understand that generics in Java are implemented using a technique called "type erasure." This means that generic type information is only available at compile time and is erased at runtime. After compilation, all generic types are replaced with their bounds or Object if no bounds are specified.

This has several implications:

  1. You cannot use instanceof with generic types
  2. You cannot create arrays of generic types
  3. Static fields are shared between all instances of the generic class

Best Practices for Using Generics

  1. Use meaningful type parameter names for better code readability
  2. Design for extension by using bounded wildcards appropriately
  3. Prefer Lists to arrays when working with generics
  4. Design APIs with PECS: "Producer Extends, Consumer Super"
    • Use <? extends T> when you only need to get values
    • Use <? super T> when you only need to put values
  5. Don't overuse generics - they add complexity and shouldn't be used when a simple solution works

Summary

Java Generics provide a powerful way to create reusable, type-safe code. We've covered:

  • Basic syntax and benefits of generics
  • Creating generic classes and methods
  • Bounded type parameters
  • Wildcard types and their use cases
  • Real-world applications of generics
  • Type erasure and its implications

Generics are extensively used in the Spring Framework, especially in collections, repositories, and service layers. Understanding generics is essential for creating maintainable and robust Java applications.

Exercises

  1. Create a generic Pair<K, V> class that can hold two values of different types
  2. Implement a generic Stack<E> with push, pop, and peek methods
  3. Write a generic method that swaps two elements in an array
  4. Create a bounded generic class that works only with classes that implement Comparable
  5. Implement a simplified generic Optional<T> class with methods like isPresent(), get(), and orElse(T other)

Additional Resources



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)