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:
- Type Safety: Catch type errors at compile-time rather than runtime
- Elimination of Casting: No need for explicit casting when retrieving elements
- Code Reusability: Write algorithms that work with different types
- 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 TypeE
for ElementK
for KeyV
for ValueN
for Number
Let's start with a simple example:
// 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:
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:
// 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:
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:
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:
// 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:
// 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:
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:
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:
- Unbounded Wildcard:
<?>
- Upper Bounded Wildcard:
<? extends Type>
- Lower Bounded Wildcard:
<? super Type>
Unbounded Wildcard
Use when you want to accept any type:
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:
// 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:
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:
// 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:
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:
// 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:
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:
- You cannot use
instanceof
with generic types - You cannot create arrays of generic types
- Static fields are shared between all instances of the generic class
Best Practices for Using Generics
- Use meaningful type parameter names for better code readability
- Design for extension by using bounded wildcards appropriately
- Prefer Lists to arrays when working with generics
- 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
- Use
- 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
- Create a generic
Pair<K, V>
class that can hold two values of different types - Implement a generic
Stack<E>
with push, pop, and peek methods - Write a generic method that swaps two elements in an array
- Create a bounded generic class that works only with classes that implement
Comparable
- Implement a simplified generic
Optional<T>
class with methods likeisPresent()
,get()
, andorElse(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! :)