Java Generics: In-Depth Insights with Practical Implementations
Janaka Wijerathne
Software Engineer at Virtusa | Java, Spring Boot, Microservices, Full-Stack Developments
Generics in Java are a powerful feature that enables developers to write flexible, reusable, and type-safe code. This article will delve deeply into the various aspects of generics in Java, providing complex examples and explaining each concept in a straightforward manner. We will cover:
1. Introduction to Generics
Generics allow types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. This helps in creating code that works with any type and ensures type safety at compile-time.
Important Points to Consider:
Example: Using Generics with Collections
import java.util.ArrayList;
import java.util.List;
public class GenericsExample {
public static void main(String[] args) {
// List of Strings ensures type safety
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// Compile-time type checking
for (String name : names) {
System.out.println(name);
}
// List of Integers (using Integer instead of int)
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
// Type inference by the compiler
for (int number : numbers) {
System.out.println(number);
}
}
}
Here, the List ensures that only String objects are added, preventing runtime errors.
2. Generic Classes
Generic classes enable you to create classes that can operate on any type specified at instantiation.
Important Points to Consider:
Example: Generic Pair Class
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public static void main(String[] args) {
// Creating a Pair of String and Integer
Pair<String, Integer> pair = new Pair<>("One", 1);
System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());
// Creating a Pair of two Strings
Pair<String, String> stringPair = new Pair<>("Hello", "World");
System.out.println("Key: " + stringPair.getKey() + ", Value: " + stringPair.getValue());
}
}
Here, Pair can hold two related objects of any type.
3. Generic Methods
Generic methods allow you to create methods that can operate on any type.
Important Points to Consider:
Example: Counting Occurrences in an Array
public class GenericsUtility {
// Generic method with type parameter <T>
public static <T> int countOccurrences(T[] array, T item) {
int count = 0;
for (T element : array) {
if (element.equals(item)) {
count++;
}
}
return count;
}
public static void main(String[] args) {
// Using generic method with Integer array
Integer[] intArray = {1, 2, 3, 1, 4, 1};
System.out.println("Count of 1: " + countOccurrences(intArray, 1)); // Output: 3
// Using generic method with String array
String[] stringArray = {"apple", "banana", "apple"};
System.out.println("Count of apple: " + countOccurrences(stringArray, "apple")); // Output: 2
}
}
4. Bounded Type Parameters
Bounded type parameters restrict the types that can be used as generics, providing more control.
Important Points to Consider:
Example: Counting Numbers Greater Than a Given Value
import java.math.BigDecimal;
import java.util.List;
import java.util.ArrayList;
// Upper Bounds: Use <T extends Class> to restrict to subclasses of a specific class.
class UpperBoundExample {
public static <T extends Number> void printNumbers(List<T> list) {
for (Number n : list) {
System.out.println(n);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
printNumbers(intList); // Works with Integer list
}
}
// Multiple Bounds: Use <T extends Class1 & Interface1> for multiple bounds.
interface Valuable {
BigDecimal getValue();
}
class MultipleBoundExample<T extends Number & Valuable> {
private T item;
public MultipleBoundExample(T item) {
this.item = item;
}
public void displayValue() {
System.out.println("Value: " + item.getValue());
}
public static void main(String[] args) {
class Price implements Valuable {
private BigDecimal value;
public Price(BigDecimal value) {
this.value = value;
}
@Override
public BigDecimal getValue() {
return value;
}
}
Price price = new Price(BigDecimal.valueOf(99.99));
MultipleBoundExample<Price> example = new MultipleBoundExample<>(price);
example.displayValue(); // Output: Value: 99.99
}
}
// Lower Bounds: Use super keyword in wildcards for lower bounds.
class LowerBoundExample {
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
addNumbers(numList);
for (Number num : numList) {
System.out.println(num); // Output: 1, 2
}
}
}
Here, T is bounded by Comparable<T>, ensuring that the elements can be compared. The use of BigDecimal highlights how generics can handle complex types in a type-safe manner.
5. Generic Interfaces
Generic interfaces allow you to define interfaces that can be implemented by any class with a specific type.
Important Points to Consider:
Example: Generic Container Interface
public interface Container<T> {
void add(T item);
T get(int index);
}
class StringContainer implements Container<String> {
private List<String> items = new ArrayList<>();
@Override
public void add(String item) {
items.add(item);
}
@Override
public String get(int index) {
return items.get(index);
}
}
class Main {
public static void main(String[] args) {
Container<String> stringContainer = new StringContainer();
stringContainer.add("Hello");
System.out.println("String: " + stringContainer.get(0));
}
}
6. Wildcards in Generics
Wildcards provide flexibility in generics, especially when working with collections of unknown types.
Important Points to Consider:
Example: Demonstrating Wildcards
import java.util.List;
import java.util.ArrayList;
// Unbounded Wildcards: Use <?> when the type is unknown.
class UnboundedWildcardExample {
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
public static void main(String[] args) {
List<String> stringList = List.of("A", "B", "C");
printList(stringList); // Works with any type of list
}
}
// Upper Bounded Wildcards: Use <? extends T> to specify an upper bound.
class UpperBoundedWildcardExample {
public static void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
printNumbers(intList); // Works with Integer list
printNumbers(doubleList); // Works with Double list
}
}
// Lower Bounded Wildcards: Use <? super T> to specify a lower bound.
class LowerBoundedWildcardExample {
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
addNumbers(numList);
for (Number num : numList) {
System.out.println(num); // Output: 1, 2
}
}
}
7. Use Cases
A. Implementing a Generic Cache
A generic cache can store objects of any type and provide efficient retrieval.
Important Points to Consider:
Example: Generic Cache Implementation
import java.util.HashMap;
import java.util.Map;
public class Cache<K, V> {
private Map<K, V> map = new HashMap<>();
public void put(K key, V value) {
map.put(key, value);
}
public V get(K key) {
return map.get(key);
}
public boolean containsKey(K key) {
return map.containsKey(key);
}
public static void main(String[] args) {
Cache<String, Integer> cache = new Cache<>();
cache.put("One", 1);
cache.put("Two", 2);
System.out.println("Key: One, Value: " + cache.get("One")); // Output: Key: One, Value: 1
System.out.println("Contains 'Two': " + cache.containsKey("Two")); // Output: Contains 'Two': true
}
}
B. Using Generics with Comparator
Generics can be used with Comparator to create flexible sorting logic.
Important Points to Consider:
Example: Sorting with a Generic Comparator
import java.util.*;
public class Sorter {
public static <T extends Comparable<T>> void sortList(List<T> list) {
Collections.sort(list);
}
public static void main(String[] args) {
List<String> stringList = Arrays.asList("Banana", "Apple", "Cherry");
sortList(stringList);
System.out.println(stringList); // Output: [Apple, Banana, Cherry]
List<Integer> intList = Arrays.asList(3, 1, 4, 1, 5);
sortList(intList);
System.out.println(intList); // Output: [1, 1, 3, 4, 5]
}
}
C. Multi-Level Generics
Generics can be used in complex scenarios involving multiple levels.
Important Points to Consider:
Example: Multi-Level Generics
class Response<T> {
private T data;
public Response(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
class PaginatedResponse<T> extends Response<List<T>> {
private int totalCount;
public PaginatedResponse(List<T> data, int totalCount) {
super(data);
this.totalCount = totalCount;
}
public int getTotalCount() {
return totalCount;
}
}
class Main {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob");
PaginatedResponse<String> response = new PaginatedResponse<>(names, names.size());
System.out.println("Total Count: " + response.getTotalCount()); // Output: Total Count: 2
System.out.println("Data: " + response.getData()); // Output: Data: [Alice, Bob]
}
}
D. Generic Utilities for BigDecimal
Generics can be particularly useful when dealing with financial calculations, where BigDecimal is often used.
Important Points to Consider:
Example: Calculating Sum and Average with BigDecimal
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
public class BigDecimalUtils {
public static BigDecimal sum(List<BigDecimal> numbers) {
BigDecimal total = BigDecimal.ZERO;
for (BigDecimal number : numbers) {
total = total.add(number);
}
return total;
}
public static BigDecimal average(List<BigDecimal> numbers) {
if (numbers.isEmpty()) {
return BigDecimal.ZERO;
}
BigDecimal total = sum(numbers);
return total.divide(BigDecimal.valueOf(numbers.size()), BigDecimal.ROUND_HALF_UP);
}
public static void main(String[] args) {
List<BigDecimal> numbers = Arrays.asList(
new BigDecimal("10.5"),
new BigDecimal("20.75"),
new BigDecimal("30.25")
);
System.out.println("Sum: " + sum(numbers)); // Output: Sum: 61.50
System.out.println("Average: " + average(numbers)); // Output: Average: 20.50
}
}
Conclusion
Generics in Java are a versatile and powerful feature that enables the creation of flexible, reusable, and type-safe code. By understanding and effectively utilizing generic classes, methods, interfaces, and wildcards, you can significantly enhance your Java programming skills. The examples provided in this article demonstrate various complex use cases, illustrating the full potential of generics in real-world scenarios.
Student at G.Narayanamma institute of Technology and Science for Women|IT Department| Junior Software Test Automation Engineer at EPAM systems
2 个月Thank you so much for sharing this
Student at IIC university of cambodia
8 个月Thanks for sharing??