Progress11 of 16 topics

69% complete

Generics in Java

Generics in Java enable you to create classes, interfaces, and methods that operate on types as parameters. Introduced in Java 5, generics add compile-time type safety and eliminate the need for explicit type casting, making your code more robust and reusable.

What You'll Learn

  • Understanding the purpose and benefits of generics
  • Creating and using generic classes and interfaces
  • Implementing generic methods
  • Working with bounded type parameters
  • Understanding wildcards and their usage
  • Type erasure and its implications
  • Best practices and common pitfalls

Why Use Generics?

Before generics, you would use Object to create "generic" data structures, requiring explicit casting and risking runtime errors:

java
1// Before generics (Java 1.4 and earlier)
2ArrayList list = new ArrayList();
3list.add("Hello"); // Adding a String
4list.add(42); // Adding an Integer
5list.add(true); // Adding a Boolean
6
7// Retrieving elements requires explicit casting
8String str = (String) list.get(0); // OK
9String num = (String) list.get(1); // ClassCastException at runtime!

With generics, you get compile-time type safety:

java
1// With generics (Java 5 and later)
2ArrayList<String> list = new ArrayList<>();
3list.add("Hello"); // OK
4list.add(42); // Compile-time error!
5list.add(true); // Compile-time error!
6
7// No casting needed
8String str = list.get(0); // Type safety guaranteed

Benefits of Generics

  • Stronger type checks at compile time
  • Elimination of explicit casting
  • Enabling implementation of generic algorithms
  • Code reuse across different types
  • Better API design with clearer intent
  • Enhanced IDE support and code documentation

Common Use Cases

  • Collection classes (List, Set, Map, etc.)
  • Custom data structures (trees, graphs, etc.)
  • Utility classes that operate on different types
  • Methods that need to preserve type information
  • Functional interfaces and lambda expressions
  • API design for frameworks and libraries

Generic Classes and Interfaces

You can create your own generic classes and interfaces by specifying one or more type parameters:

java
1// A simple generic class with one type parameter
2public class Box<T> {
3 private T content;
4
5 public Box(T content) {
6 this.content = content;
7 }
8
9 public T getContent() {
10 return content;
11 }
12
13 public void setContent(T content) {
14 this.content = content;
15 }
16}
17
18// Using the generic Box class
19Box<String> stringBox = new Box<>("Hello, generics!");
20String content = stringBox.getContent(); // No casting needed
21
22Box<Integer> intBox = new Box<>(42);
23Integer number = intBox.getContent(); // Type safety guaranteed

Multiple Type Parameters

Generic classes can have multiple type parameters:

java
1// Generic class with multiple type parameters
2public class Pair<K, V> {
3 private K key;
4 private V value;
5
6 public Pair(K key, V value) {
7 this.key = key;
8 this.value = value;
9 }
10
11 public K getKey() {
12 return key;
13 }
14
15 public V getValue() {
16 return value;
17 }
18
19 @Override
20 public String toString() {
21 return "(" + key + ", " + value + ")";
22 }
23}
24
25// Using the Pair class
26Pair<String, Integer> student = new Pair<>("John", 95);
27String name = student.getKey();
28Integer score = student.getValue();
29
30// Since Java 7, you can use the diamond operator <>
31Pair<String, Double> product = new Pair<>("Laptop", 999.99);

Generic Interfaces

Like classes, interfaces can also be generic:

java
1// Generic interface
2public interface Repository<T, ID> {
3 T findById(ID id);
4 void save(T entity);
5 void delete(T entity);
6 List<T> findAll();
7}
8
9// Implementing a generic interface
10public class UserRepository implements Repository<User, Long> {
11 @Override
12 public User findById(Long id) {
13 // Implementation to find a user by ID
14 return new User(id);
15 }
16
17 @Override
18 public void save(User user) {
19 // Implementation to save a user
20 }
21
22 @Override
23 public void delete(User user) {
24 // Implementation to delete a user
25 }
26
27 @Override
28 public List<User> findAll() {
29 // Implementation to find all users
30 return new ArrayList<>();
31 }
32}

Naming Convention for Type Parameters

By convention, type parameter names are single, uppercase letters:

  • T - Type (general purpose)
  • E - Element (used in collections)
  • K - Key (used in maps)
  • V - Value (used in maps)
  • N - Number
  • S, U, V etc. - Additional types

Generic Methods

Methods can also use type parameters, independent of the class's type parameters:

java
1// Generic method
2public class Utilities {
3 // A generic method that works with any type
4 public static <T> T getMiddleElement(T[] array) {
5 if (array == null || array.length == 0) {
6 return null;
7 }
8 return array[array.length / 2];
9 }
10
11 // Generic method with multiple type parameters
12 public static <K, V> Map<K, V> zipToMap(K[] keys, V[] values) {
13 if (keys.length != values.length) {
14 throw new IllegalArgumentException("Arrays must be of the same length");
15 }
16
17 Map<K, V> map = new HashMap<>();
18 for (int i = 0; i < keys.length; i++) {
19 map.put(keys[i], values[i]);
20 }
21
22 return map;
23 }
24}
25
26// Using generic methods
27String[] names = {"Alice", "Bob", "Charlie", "Dave", "Eve"};
28String middle = Utilities.<String>getMiddleElement(names); // Explicit type argument
29String middle2 = Utilities.getMiddleElement(names); // Type inference (preferred)
30
31Integer[] ids = {1, 2, 3};
32String[] roles = {"Admin", "User", "Guest"};
33Map<Integer, String> userRoles = Utilities.zipToMap(ids, roles);

Bounded Type Parameters

You can restrict the types that can be used with a generic class or method using bounded type parameters:

java
1// Upper bound: T must be a Number or a subclass of Number
2public class MathBox<T extends Number> {
3 private T value;
4
5 public MathBox(T value) {
6 this.value = value;
7 }
8
9 public double sqrt() {
10 // Can use Number methods because T is bounded by Number
11 return Math.sqrt(value.doubleValue());
12 }
13
14 public T getValue() {
15 return value;
16 }
17}
18
19// Using bounded types
20MathBox<Integer> intBox = new MathBox<>(16);
21System.out.println(intBox.sqrt()); // Output: 4.0
22
23MathBox<Double> doubleBox = new MathBox<>(25.0);
24System.out.println(doubleBox.sqrt()); // Output: 5.0
25
26// This would not compile - String is not a subclass of Number
27// MathBox<String> stringBox = new MathBox<>("Hello"); // Compile error!

Multiple Bounds

A type parameter can have multiple bounds using the & operator:

java
1// Interface bounds
2interface Drawable {
3 void draw();
4}
5
6interface Resizable {
7 void resize(int width, int height);
8}
9
10// Multiple bounds: T must implement both Drawable and Resizable
11public class Shape<T extends Drawable & Resizable> {
12 private T item;
13
14 public Shape(T item) {
15 this.item = item;
16 }
17
18 public void drawAndResize(int width, int height) {
19 item.draw();
20 item.resize(width, height);
21 }
22}
23
24// Class implementing both interfaces
25class Rectangle implements Drawable, Resizable {
26 @Override
27 public void draw() {
28 System.out.println("Drawing rectangle");
29 }
30
31 @Override
32 public void resize(int width, int height) {
33 System.out.println("Resizing rectangle to " + width + "x" + height);
34 }
35}
36
37// Using multiple bounds
38Shape<Rectangle> rectangleShape = new Shape<>(new Rectangle());
39rectangleShape.drawAndResize(100, 50);

Note: If one of the bounds is a class, it must be listed first in the bounds list. For example: <T extends BaseClass & Interface1 & Interface2>

Wildcards

Wildcards allow for more flexible generic code by representing an unknown type:

Unbounded Wildcards

Use ? when you want to work with objects of an unknown type:

java
1// Unbounded wildcard
2public static void printList(List<?> list) {
3 for (Object item : list) {
4 System.out.println(item);
5 }
6}
7
8// Using unbounded wildcard
9List<String> strings = Arrays.asList("Java", "Generics", "Wildcards");
10List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
11
12printList(strings); // Works with List<String>
13printList(numbers); // Works with List<Integer>

Upper Bounded Wildcards

Use ? extends Type when you want to work with a type or its subtypes:

java
1// Upper bounded wildcard
2public static double sumOfList(List<? extends Number> list) {
3 double sum = 0.0;
4 for (Number number : list) {
5 sum += number.doubleValue();
6 }
7 return sum;
8}
9
10// Using upper bounded wildcards
11List<Integer> integers = Arrays.asList(1, 2, 3);
12List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
13
14System.out.println(sumOfList(integers)); // Works with List<Integer>
15System.out.println(sumOfList(doubles)); // Works with List<Double>

Lower Bounded Wildcards

Use ? super Type when you want to work with a type or its supertypes:

java
1// Lower bounded wildcard
2public static void addNumbers(List<? super Integer> list) {
3 for (int i = 1; i <= 5; i++) {
4 list.add(i); // Can add Integers to the list
5 }
6}
7
8// Using lower bounded wildcards
9List<Integer> integers = new ArrayList<>();
10List<Number> numbers = new ArrayList<>();
11List<Object> objects = new ArrayList<>();
12
13addNumbers(integers); // Works with List<Integer>
14addNumbers(numbers); // Works with List<Number>
15addNumbers(objects); // Works with List<Object>

PECS Principle: Producer Extends, Consumer Super

This guideline helps you decide which kind of bounded wildcard to use:

  • Use ? extends T when you only need to read from a collection (the collection produces values for you)
  • Use ? super T when you only need to write to a collection (the collection consumes values from you)
  • Use exact type when you need to both read and write

Type Erasure

Java's generics are implemented using a technique called type erasure. During compilation, all generic type information is removed and replaced with:

  • The upper bound if specified (e.g., Number for <T extends Number>)
  • Otherwise, Object
java
1// Generic class before type erasure
2public class Box<T> {
3 private T content;
4
5 public T getContent() {
6 return content;
7 }
8
9 public void setContent(T content) {
10 this.content = content;
11 }
12}
13
14// After type erasure (conceptually)
15public class Box {
16 private Object content;
17
18 public Object getContent() {
19 return content;
20 }
21
22 public void setContent(Object content) {
23 this.content = content;
24 }
25}

Implications of Type Erasure

Type erasure has several implications that you need to be aware of:

Cannot Create Instances of Type Parameters

You cannot create an instance of a type parameter:

// Not allowed
public <T> T create() {
  return new T(); // Compile error!
}

Cannot Use instanceof with Generic Types

Cannot use instanceof with generic types at runtime:

// Not allowed
if (obj instanceof List<String>) { // Compile error!
  // ... }

// This is allowed
if (obj instanceof List<?>) {
  // ... }

Cannot Create Arrays of Generic Types

Cannot create arrays of generic types:

// Not allowed
List<String>[] array = new List<String>[10]; // Compile error!

// This is allowed
List<?>[] array = new List<?>[10];

Cannot Overload Methods with Different Generic Types

Cannot overload methods that would have the same erasure:

// Not allowed
public void process(List<String> list) { ... }
public void process(List<Integer> list) { ... } // Compile error!

Best Practices

Effective Use of Generics

  • Use generics for collections: Always specify the type parameter for collections to ensure type safety.
  • Use bounded wildcards appropriately: Apply the PECS principle (Producer Extends, Consumer Super).
  • Minimize wildcard usage: Use exact types when possible to make the API clearer.
  • Provide factory methods: To overcome the inability to create instances of type parameters, use factory methods or pass Class objects.
  • Design for inheritance: When designing generic classes for inheritance, consider making the subclass generic as well.
  • Document restrictions: Clearly document any restrictions on type parameters in your API documentation.

Real-World Example: Generic Data Cache

Here's a practical example of a generic data cache that can store and retrieve values of any type:

java
1import java.util.HashMap;
2import java.util.Map;
3import java.util.Optional;
4import java.util.concurrent.ConcurrentHashMap;
5import java.util.function.Function;
6
7/**
8 * A generic cache that can store and retrieve values of any type.
9 * @param <K> the type of keys
10 * @param <V> the type of values
11 */
12public class DataCache<K, V> {
13 private final Map<K, V> cache;
14 private final Function<K, V> dataLoader;
15
16 /**
17 * Creates a new cache with the specified data loader function.
18 * @param dataLoader function to load data when not found in cache
19 */
20 public DataCache(Function<K, V> dataLoader) {
21 this.cache = new ConcurrentHashMap<>();
22 this.dataLoader = dataLoader;
23 }
24
25 /**
26 * Gets a value from the cache, loading it if not present.
27 * @param key the key to look up
28 * @return the value associated with the key
29 */
30 public V get(K key) {
31 return cache.computeIfAbsent(key, dataLoader);
32 }
33
34 /**
35 * Gets a value from the cache if present, without loading.
36 * @param key the key to look up
37 * @return an Optional containing the value, or empty if not in cache
38 */
39 public Optional<V> getIfPresent(K key) {
40 return Optional.ofNullable(cache.get(key));
41 }
42
43 /**
44 * Puts a value into the cache.
45 * @param key the key
46 * @param value the value
47 * @return the previous value, or null if none
48 */
49 public V put(K key, V value) {
50 return cache.put(key, value);
51 }
52
53 /**
54 * Removes a value from the cache.
55 * @param key the key to remove
56 * @return the removed value, or null if none
57 */
58 public V remove(K key) {
59 return cache.remove(key);
60 }
61
62 /**
63 * Clears all entries from the cache.
64 */
65 public void clear() {
66 cache.clear();
67 }
68
69 /**
70 * Gets the number of entries in the cache.
71 * @return the size of the cache
72 */
73 public int size() {
74 return cache.size();
75 }
76}
77
78// Example usage
79public class DataCacheExample {
80 public static void main(String[] args) {
81 // Create a cache for expensive database operations
82 DataCache<String, User> userCache = new DataCache<>(id -> {
83 System.out.println("Loading user with ID: " + id);
84 // In a real app, this would query a database
85 return new User(id, "User " + id);
86 });
87
88 // First access: data will be loaded
89 User user1 = userCache.get("1001");
90 System.out.println("Got user: " + user1.getName());
91
92 // Second access: data comes from cache
93 User cachedUser = userCache.get("1001");
94 System.out.println("Got user: " + cachedUser.getName());
95
96 // Put a new user directly into the cache
97 userCache.put("1002", new User("1002", "Manual User"));
98
99 // Check if a user exists in the cache
100 Optional<User> optionalUser = userCache.getIfPresent("1003");
101 if (optionalUser.isPresent()) {
102 System.out.println("Found: " + optionalUser.get().getName());
103 } else {
104 System.out.println("User not in cache");
105 }
106 }
107}
108
109class User {
110 private String id;
111 private String name;
112
113 public User(String id, String name) {
114 this.id = id;
115 this.name = name;
116 }
117
118 public String getId() {
119 return id;
120 }
121
122 public String getName() {
123 return name;
124 }
125}

Summary

Java generics are a powerful feature that enable type-safe, reusable code:

  • Generics provide compile-time type safety and eliminate casting
  • You can create generic classes, interfaces, and methods
  • Bounded type parameters restrict the types that can be used
  • Wildcards make generic code more flexible
  • Type erasure removes generic type information at runtime
  • Understanding the implications of type erasure helps avoid common pitfalls
  • Following best practices leads to cleaner, more maintainable code

Mastering generics is essential for writing modern Java code, especially when working with collections, frameworks, and APIs. The Java Collections Framework makes extensive use of generics, so this knowledge will be directly applicable to your everyday Java programming.

Related Tutorials

Learn about the Java Collections Framework that extensively uses generics.

Learn more

Understand fundamental OOP concepts in Java.

Learn more

Master interfaces, a key component in Java programming.

Learn more