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:
1// Before generics (Java 1.4 and earlier)2ArrayList list = new ArrayList();3list.add("Hello"); // Adding a String4list.add(42); // Adding an Integer5list.add(true); // Adding a Boolean67// Retrieving elements requires explicit casting8String str = (String) list.get(0); // OK9String num = (String) list.get(1); // ClassCastException at runtime!
With generics, you get compile-time type safety:
1// With generics (Java 5 and later)2ArrayList<String> list = new ArrayList<>();3list.add("Hello"); // OK4list.add(42); // Compile-time error!5list.add(true); // Compile-time error!67// No casting needed8String 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:
1// A simple generic class with one type parameter2public class Box<T> {3 private T content;45 public Box(T content) {6 this.content = content;7 }89 public T getContent() {10 return content;11 }1213 public void setContent(T content) {14 this.content = content;15 }16}1718// Using the generic Box class19Box<String> stringBox = new Box<>("Hello, generics!");20String content = stringBox.getContent(); // No casting needed2122Box<Integer> intBox = new Box<>(42);23Integer number = intBox.getContent(); // Type safety guaranteed
Multiple Type Parameters
Generic classes can have multiple type parameters:
1// Generic class with multiple type parameters2public class Pair<K, V> {3 private K key;4 private V value;56 public Pair(K key, V value) {7 this.key = key;8 this.value = value;9 }1011 public K getKey() {12 return key;13 }1415 public V getValue() {16 return value;17 }1819 @Override20 public String toString() {21 return "(" + key + ", " + value + ")";22 }23}2425// Using the Pair class26Pair<String, Integer> student = new Pair<>("John", 95);27String name = student.getKey();28Integer score = student.getValue();2930// 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:
1// Generic interface2public interface Repository<T, ID> {3 T findById(ID id);4 void save(T entity);5 void delete(T entity);6 List<T> findAll();7}89// Implementing a generic interface10public class UserRepository implements Repository<User, Long> {11 @Override12 public User findById(Long id) {13 // Implementation to find a user by ID14 return new User(id);15 }1617 @Override18 public void save(User user) {19 // Implementation to save a user20 }2122 @Override23 public void delete(User user) {24 // Implementation to delete a user25 }2627 @Override28 public List<User> findAll() {29 // Implementation to find all users30 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
- NumberS
,U
,V
etc. - Additional types
Generic Methods
Methods can also use type parameters, independent of the class's type parameters:
1// Generic method2public class Utilities {3 // A generic method that works with any type4 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 }1011 // Generic method with multiple type parameters12 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 }1617 Map<K, V> map = new HashMap<>();18 for (int i = 0; i < keys.length; i++) {19 map.put(keys[i], values[i]);20 }2122 return map;23 }24}2526// Using generic methods27String[] names = {"Alice", "Bob", "Charlie", "Dave", "Eve"};28String middle = Utilities.<String>getMiddleElement(names); // Explicit type argument29String middle2 = Utilities.getMiddleElement(names); // Type inference (preferred)3031Integer[] 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:
1// Upper bound: T must be a Number or a subclass of Number2public class MathBox<T extends Number> {3 private T value;45 public MathBox(T value) {6 this.value = value;7 }89 public double sqrt() {10 // Can use Number methods because T is bounded by Number11 return Math.sqrt(value.doubleValue());12 }1314 public T getValue() {15 return value;16 }17}1819// Using bounded types20MathBox<Integer> intBox = new MathBox<>(16);21System.out.println(intBox.sqrt()); // Output: 4.02223MathBox<Double> doubleBox = new MathBox<>(25.0);24System.out.println(doubleBox.sqrt()); // Output: 5.02526// This would not compile - String is not a subclass of Number27// MathBox<String> stringBox = new MathBox<>("Hello"); // Compile error!
Multiple Bounds
A type parameter can have multiple bounds using the & operator:
1// Interface bounds2interface Drawable {3 void draw();4}56interface Resizable {7 void resize(int width, int height);8}910// Multiple bounds: T must implement both Drawable and Resizable11public class Shape<T extends Drawable & Resizable> {12 private T item;1314 public Shape(T item) {15 this.item = item;16 }1718 public void drawAndResize(int width, int height) {19 item.draw();20 item.resize(width, height);21 }22}2324// Class implementing both interfaces25class Rectangle implements Drawable, Resizable {26 @Override27 public void draw() {28 System.out.println("Drawing rectangle");29 }3031 @Override32 public void resize(int width, int height) {33 System.out.println("Resizing rectangle to " + width + "x" + height);34 }35}3637// Using multiple bounds38Shape<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:
1// Unbounded wildcard2public static void printList(List<?> list) {3 for (Object item : list) {4 System.out.println(item);5 }6}78// Using unbounded wildcard9List<String> strings = Arrays.asList("Java", "Generics", "Wildcards");10List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);1112printList(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:
1// Upper bounded wildcard2public 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}910// Using upper bounded wildcards11List<Integer> integers = Arrays.asList(1, 2, 3);12List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);1314System.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:
1// Lower bounded wildcard2public static void addNumbers(List<? super Integer> list) {3 for (int i = 1; i <= 5; i++) {4 list.add(i); // Can add Integers to the list5 }6}78// Using lower bounded wildcards9List<Integer> integers = new ArrayList<>();10List<Number> numbers = new ArrayList<>();11List<Object> objects = new ArrayList<>();1213addNumbers(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
1// Generic class before type erasure2public class Box<T> {3 private T content;45 public T getContent() {6 return content;7 }89 public void setContent(T content) {10 this.content = content;11 }12}1314// After type erasure (conceptually)15public class Box {16 private Object content;1718 public Object getContent() {19 return content;20 }2122 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:
public <T> T create() {
return new T(); // Compile error!
}
Cannot Use instanceof with Generic Types
Cannot use instanceof with generic types at runtime:
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:
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:
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:
1import java.util.HashMap;2import java.util.Map;3import java.util.Optional;4import java.util.concurrent.ConcurrentHashMap;5import java.util.function.Function;67/**8 * A generic cache that can store and retrieve values of any type.9 * @param <K> the type of keys10 * @param <V> the type of values11 */12public class DataCache<K, V> {13 private final Map<K, V> cache;14 private final Function<K, V> dataLoader;1516 /**17 * Creates a new cache with the specified data loader function.18 * @param dataLoader function to load data when not found in cache19 */20 public DataCache(Function<K, V> dataLoader) {21 this.cache = new ConcurrentHashMap<>();22 this.dataLoader = dataLoader;23 }2425 /**26 * Gets a value from the cache, loading it if not present.27 * @param key the key to look up28 * @return the value associated with the key29 */30 public V get(K key) {31 return cache.computeIfAbsent(key, dataLoader);32 }3334 /**35 * Gets a value from the cache if present, without loading.36 * @param key the key to look up37 * @return an Optional containing the value, or empty if not in cache38 */39 public Optional<V> getIfPresent(K key) {40 return Optional.ofNullable(cache.get(key));41 }4243 /**44 * Puts a value into the cache.45 * @param key the key46 * @param value the value47 * @return the previous value, or null if none48 */49 public V put(K key, V value) {50 return cache.put(key, value);51 }5253 /**54 * Removes a value from the cache.55 * @param key the key to remove56 * @return the removed value, or null if none57 */58 public V remove(K key) {59 return cache.remove(key);60 }6162 /**63 * Clears all entries from the cache.64 */65 public void clear() {66 cache.clear();67 }6869 /**70 * Gets the number of entries in the cache.71 * @return the size of the cache72 */73 public int size() {74 return cache.size();75 }76}7778// Example usage79public class DataCacheExample {80 public static void main(String[] args) {81 // Create a cache for expensive database operations82 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 database85 return new User(id, "User " + id);86 });8788 // First access: data will be loaded89 User user1 = userCache.get("1001");90 System.out.println("Got user: " + user1.getName());9192 // Second access: data comes from cache93 User cachedUser = userCache.get("1001");94 System.out.println("Got user: " + cachedUser.getName());9596 // Put a new user directly into the cache97 userCache.put("1002", new User("1002", "Manual User"));9899 // Check if a user exists in the cache100 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}108109class User {110 private String id;111 private String name;112113 public User(String id, String name) {114 this.id = id;115 this.name = name;116 }117118 public String getId() {119 return id;120 }121122 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 moreUnderstand fundamental OOP concepts in Java.
Learn moreMaster interfaces, a key component in Java programming.
Learn more