Table of Contents
- Introduction to Generics
- Generic Class Syntax
- Type Parameters and Bounds
- Generic Methods
- Wildcards
- Inheritance with Generics
- Type Erasure
- Real-World Examples
- Best Practices
Introduction to Generics
Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. This allows for type safety and eliminates the need for casting.
Benefits of Generics
- Type Safety: Compile-time type checking
- Eliminates Casting: No need to cast objects
- Code Reusability: Write once, use with different types
- Better Algorithms: Create generic algorithms that work on collections of different types
Before and After Generics
import java.util.*;
public class GenericsIntroduction {
// Before Generics (Java 1.4 and earlier)
public static void withoutGenerics() {
List list = new ArrayList(); // Raw type
list.add("Hello");
list.add("World");
list.add(123); // No compile-time error!
// Runtime ClassCastException possible
String first = (String) list.get(0); // Explicit casting needed
// String second = (String) list.get(2); // ClassCastException!
}
// With Generics (Java 5+)
public static void withGenerics() {
List<String> list = new ArrayList<>(); // Parameterized type
list.add("Hello");
list.add("World");
// list.add(123); // Compile-time error!
String first = list.get(0); // No casting needed
String second = list.get(1); // Type safety guaranteed
}
public static void main(String[] args) {
withoutGenerics();
withGenerics();
}
}
Generic Class Syntax
Basic Generic Class
// Simple generic class with one type parameter
public class Box<T> {
private T content;
public Box() {} // Default constructor
public Box(T content) {
this.content = content;
}
// Setter
public void setContent(T content) {
this.content = content;
}
// Getter
public T getContent() {
return content;
}
// Generic method within generic class
public <U> void inspect(U value) {
System.out.println("T: " + content.getClass().getName());
System.out.println("U: " + value.getClass().getName());
}
@Override
public String toString() {
return "Box containing: " + content;
}
}
// Using the generic class
public class BasicGenericClassDemo {
public static void main(String[] args) {
// Box for String
Box<String> stringBox = new Box<>("Hello Generics");
System.out.println(stringBox);
stringBox.inspect(123); // U becomes Integer
// Box for Integer
Box<Integer> integerBox = new Box<>(42);
System.out.println(integerBox);
integerBox.inspect("Test"); // U becomes String
// Box for custom object
Box<Date> dateBox = new Box<>(new Date());
System.out.println(dateBox);
}
}
Multiple Type Parameters
// Generic class with multiple type parameters
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 void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
// Static generic method
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
@Override
public String toString() {
return "Pair{key=" + key + ", value=" + value + "}";
}
}
// Using multiple type parameters
public class MultipleTypeParamsDemo {
public static void main(String[] args) {
// String-Integer pair
Pair<String, Integer> nameAge = new Pair<>("John", 25);
System.out.println(nameAge);
// Integer-String pair
Pair<Integer, String> idName = new Pair<>(101, "Alice");
System.out.println(idName);
// Custom types
Pair<String, Date> eventTime = new Pair<>("Meeting", new Date());
System.out.println(eventTime);
// Using static generic method
Pair<String, Integer> p1 = new Pair<>("A", 1);
Pair<String, Integer> p2 = new Pair<>("A", 1);
System.out.println("Pairs equal: " + Pair.compare(p1, p2));
}
}
Type Parameters and Bounds
Bounded Type Parameters
import java.util.*;
// Upper bound - T must be Number or its subclass
public class NumberBox<T extends Number> {
private T number;
public NumberBox(T number) {
this.number = number;
}
public T getNumber() {
return number;
}
// Can safely call Number methods
public double getDoubleValue() {
return number.doubleValue();
}
public int getIntValue() {
return number.intValue();
}
// Multiple bounds
public static <T extends Number & Comparable<T>> T max(T[] array) {
if (array == null || array.length == 0) {
return null;
}
T max = array[0];
for (T element : array) {
if (element.compareTo(max) > 0) {
max = element;
}
}
return max;
}
}
// Interface bound
interface Displayable {
void display();
}
class Product implements Displayable {
private String name;
public Product(String name) {
this.name = name;
}
@Override
public void display() {
System.out.println("Product: " + name);
}
}
public class DisplayableBox<T extends Displayable> {
private T item;
public DisplayableBox(T item) {
this.item = item;
}
public void show() {
item.display(); // Can call Displayable methods
}
}
// Bounds demonstration
public class BoundsDemo {
public static void main(String[] args) {
// Valid - Integer extends Number
NumberBox<Integer> intBox = new NumberBox<>(42);
System.out.println("Double value: " + intBox.getDoubleValue());
// Valid - Double extends Number
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
System.out.println("Int value: " + doubleBox.getIntValue());
// Invalid - String doesn't extend Number
// NumberBox<String> stringBox = new NumberBox<>("Hello"); // Compile error
// Using multiple bounds
Integer[] numbers = {1, 5, 3, 9, 2};
Integer maxNumber = NumberBox.max(numbers);
System.out.println("Max number: " + maxNumber);
// Interface bound
DisplayableBox<Product> productBox = new DisplayableBox<>(new Product("Laptop"));
productBox.show();
}
}
Lower Bounds with Wildcards
import java.util.*;
public class BoundsWithWildcards {
// Upper bounded wildcard - accepts List of any type that is Number or subclass
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number number : list) {
sum += number.doubleValue();
}
return sum;
}
// Lower bounded wildcard - accepts List of Integer or supertypes
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}
// Unbounded wildcard - accepts List of any type
public static void printList(List<?> list) {
for (Object element : list) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
// Upper bound examples
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
System.out.println("Sum of integers: " + sumOfList(integers));
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("Sum of doubles: " + sumOfList(doubles));
// Lower bound examples
List<Number> numbers = new ArrayList<>();
addNumbers(numbers); // Number is supertype of Integer
System.out.println("Numbers after adding: " + numbers);
List<Object> objects = new ArrayList<>();
addNumbers(objects); // Object is supertype of Integer
System.out.println("Objects after adding: " + objects);
// Unbounded examples
List<String> strings = Arrays.asList("A", "B", "C");
printList(strings);
printList(integers);
}
}
Generic Methods
Generic Method Syntax
import java.util.*;
public class GenericMethods {
// Basic generic method
public static <T> T getFirstElement(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
// Generic method with multiple type parameters
public static <K, V> void printPair(K key, V value) {
System.out.println("Key: " + key + ", Value: " + value);
}
// Bounded generic method
public static <T extends Comparable<T>> T findMax(T[] array) {
if (array == null || array.length == 0) {
return null;
}
T max = array[0];
for (T element : array) {
if (element.compareTo(max) > 0) {
max = element;
}
}
return max;
}
// Generic method with wildcards
public static void processList(List<? extends Number> list) {
for (Number number : list) {
System.out.println("Processing: " + number);
}
}
// Generic constructor (not common but possible)
public static class Container<T> {
private T value;
// Generic constructor
public <U extends T> Container(U value) {
this.value = value;
}
public T getValue() {
return value;
}
}
public static void main(String[] args) {
// Using generic methods
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
String first = getFirstElement(names);
System.out.println("First name: " + first);
List<Integer> ages = Arrays.asList(25, 30, 35);
Integer firstAge = getFirstElement(ages);
System.out.println("First age: " + firstAge);
// Multiple type parameters
printPair("Name", "John");
printPair(101, "Employee ID");
// Bounded generic method
Integer[] numbers = {3, 7, 2, 9, 1};
Integer maxNumber = findMax(numbers);
System.out.println("Max number: " + maxNumber);
String[] words = {"apple", "banana", "cherry"};
String maxWord = findMax(words);
System.out.println("Max word: " + maxWord);
// Wildcard method
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
processList(doubles);
}
}
Advanced Generic Methods
import java.util.*;
public class AdvancedGenericMethods {
// Generic method with type inference
public static <T> List<T> createList(T... elements) {
List<T> list = new ArrayList<>();
for (T element : elements) {
list.add(element);
}
return list;
}
// Generic method that returns a generic type
public static <T> T[] toArray(List<T> list, Class<T> clazz) {
@SuppressWarnings("unchecked")
T[] array = (T[]) java.lang.reflect.Array.newInstance(clazz, list.size());
return list.toArray(array);
}
// Recursive generic method
public static <T extends Comparable<T>> void bubbleSort(T[] array) {
int n = array.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (array[j].compareTo(array[j + 1]) > 0) {
// Swap elements
T temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
// Generic method with wildcard and bounds
public static double sumCollection(Collection<? extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// Generic method that works with any type
public static <T> String join(String delimiter, T... elements) {
if (elements == null || elements.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < elements.length; i++) {
if (i > 0) {
sb.append(delimiter);
}
sb.append(elements[i]);
}
return sb.toString();
}
public static void main(String[] args) {
// Type inference in action
List<String> names = createList("Alice", "Bob", "Charlie");
System.out.println("Names: " + names);
List<Integer> numbers = createList(1, 2, 3, 4, 5);
System.out.println("Numbers: " + numbers);
// Convert list to array with proper type
String[] nameArray = toArray(names, String.class);
System.out.println("Array: " + Arrays.toString(nameArray));
// Bubble sort with generics
Integer[] toSort = {5, 2, 8, 1, 9};
bubbleSort(toSort);
System.out.println("Sorted: " + Arrays.toString(toSort));
// Sum collection
List<Number> mixedNumbers = Arrays.asList(1, 2.5, 3L, 4.7f);
double total = sumCollection(mixedNumbers);
System.out.println("Total: " + total);
// Join any types
String result = join(", ", "A", 1, 2.5, true);
System.out.println("Joined: " + result);
}
}
Wildcards
Wildcard Types and Usage
import java.util.*;
public class WildcardsDeepDive {
// Unbounded wildcard - useful when you don't care about the type
public static void printCollection(Collection<?> collection) {
for (Object element : collection) {
System.out.println(element);
}
}
// Upper bounded wildcard - restricts to specific type hierarchy
public static double sumNumbers(List<? extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// Lower bounded wildcard - allows adding specific types
public static void addIntegers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i);
}
}
// Wildcard with multiple bounds (not directly supported, but you can use extends with interface)
public static void processComparables(List<? extends Comparable<?>> list) {
if (!list.isEmpty()) {
System.out.println("First element: " + list.get(0));
}
}
// Complex wildcard scenario
public static void copyNumbers(List<? extends Number> source,
List<? super Number> destination) {
for (Number number : source) {
destination.add(number);
}
}
// Wildcard in return type (rare, but possible)
public static List<?> createWildcardList() {
return Arrays.asList("A", 1, 2.5, true);
}
public static void main(String[] args) {
// Unbounded wildcard examples
List<String> strings = Arrays.asList("A", "B", "C");
List<Integer> integers = Arrays.asList(1, 2, 3);
printCollection(strings);
printCollection(integers);
// Upper bounded examples
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
System.out.println("Sum ints: " + sumNumbers(ints));
System.out.println("Sum doubles: " + sumNumbers(doubles));
// Lower bounded examples
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addIntegers(numbers);
addIntegers(objects);
System.out.println("Numbers: " + numbers);
System.out.println("Objects: " + objects);
// Copy between different number types
List<Integer> sourceList = Arrays.asList(1, 2, 3);
List<Number> destList = new ArrayList<>();
copyNumbers(sourceList, destList);
System.out.println("Copied: " + destList);
// Wildcard return
List<?> wildList = createWildcardList();
printCollection(wildList);
}
}
Wildcard Guidelines (PECS)
import java.util.*;
public class PECSPriciple {
/*
* PECS: Producer Extends, Consumer Super
* - Use <? extends T> for producers (you read from them)
* - Use <? super T> for consumers (you write to them)
* - Use <?> when you both read and write (but be careful)
*/
// Producer - uses extends (read-only)
public static double sumProducer(List<? extends Number> numbers) {
double sum = 0;
for (Number num : numbers) { // Reading from producer
sum += num.doubleValue();
}
return sum;
}
// Consumer - uses super (write-only)
public static void fillConsumer(List<? super Integer> list, int count) {
for (int i = 0; i < count; i++) {
list.add(i); // Writing to consumer
}
}
// Both producer and consumer - avoid wildcards if possible
public static <T> void copy(List<? extends T> source, List<? super T> destination) {
for (T item : source) { // Reading from producer
destination.add(item); // Writing to consumer
}
}
// Example demonstrating PECS
public static class CollectionsUtils {
// Producer example - reading elements
public static <T> T getFirst(List<? extends T> list) {
return list.isEmpty() ? null : list.get(0);
}
// Consumer example - adding elements
public static <T> void addAll(List<? super T> destination, List<? extends T> source) {
destination.addAll(source);
}
// Complex PECS example
public static <T> void copyWithFilter(List<? extends T> source,
List<? super T> destination,
java.util.function.Predicate<? super T> predicate) {
for (T element : source) {
if (predicate.test(element)) {
destination.add(element);
}
}
}
}
public static void main(String[] args) {
// Producer example
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
double sum = sumProducer(integers);
System.out.println("Sum: " + sum);
// Consumer example
List<Number> numbers = new ArrayList<>();
fillConsumer(numbers, 5);
System.out.println("Filled numbers: " + numbers);
// Copy with PECS
List<Integer> source = Arrays.asList(1, 2, 3, 4, 5);
List<Number> destination = new ArrayList<>();
copy(source, destination);
System.out.println("Copied: " + destination);
// Collections utils examples
String first = CollectionsUtils.getFirst(Arrays.asList("A", "B", "C"));
System.out.println("First: " + first);
List<Object> objects = new ArrayList<>();
CollectionsUtils.addAll(objects, integers);
System.out.println("Objects after addAll: " + objects);
// Copy with filter
List<Integer> evenNumbers = new ArrayList<>();
CollectionsUtils.copyWithFilter(integers, evenNumbers, n -> n % 2 == 0);
System.out.println("Even numbers: " + evenNumbers);
}
}
Inheritance with Generics
Generic Class Inheritance
import java.util.*;
// Base generic class
public class GenericBase<T> {
protected T value;
public GenericBase(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public void display() {
System.out.println("Value: " + value);
}
}
// Option 1: Non-generic subclass with specific type
class StringContainer extends GenericBase<String> {
public StringContainer(String value) {
super(value);
}
// Additional methods specific to String
public int getLength() {
return value.length();
}
}
// Option 2: Generic subclass with same type parameter
class EnhancedContainer<T> extends GenericBase<T> {
private String description;
public EnhancedContainer(T value, String description) {
super(value);
this.description = description;
}
public String getDescription() {
return description;
}
@Override
public void display() {
System.out.println(description + ": " + value);
}
}
// Option 3: Generic subclass with additional type parameters
class PairContainer<T, U> extends GenericBase<T> {
private U secondValue;
public PairContainer(T firstValue, U secondValue) {
super(firstValue);
this.secondValue = secondValue;
}
public U getSecondValue() {
return secondValue;
}
public void setSecondValue(U secondValue) {
this.secondValue = secondValue;
}
@Override
public void display() {
System.out.println("First: " + value + ", Second: " + secondValue);
}
}
// Option 4: Subclass with bounded type parameter
class NumberContainer<T extends Number> extends GenericBase<T> {
public NumberContainer(T value) {
super(value);
}
public double getDoubleValue() {
return value.doubleValue();
}
}
// Inheritance demonstration
public class GenericInheritanceDemo {
public static void main(String[] args) {
// Non-generic subclass
StringContainer stringBox = new StringContainer("Hello");
stringBox.display();
System.out.println("Length: " + stringBox.getLength());
// Generic subclass with same parameter
EnhancedContainer<Integer> enhanced = new EnhancedContainer<>(42, "Answer");
enhanced.display();
// Generic subclass with additional parameters
PairContainer<String, Integer> pair = new PairContainer<>("Age", 25);
pair.display();
// Bounded generic subclass
NumberContainer<Double> numberBox = new NumberContainer<>(3.14);
numberBox.display();
System.out.println("Double value: " + numberBox.getDoubleValue());
}
}
Generic Interface Implementation
import java.util.*;
// Generic interface
interface Repository<T, ID> {
void save(T entity);
T findById(ID id);
void delete(ID id);
List<T> findAll();
}
// Generic interface with multiple type parameters
interface Mapper<SOURCE, TARGET> {
TARGET map(SOURCE source);
SOURCE reverseMap(TARGET target);
}
// Implementing generic interface with specific types
class UserRepository implements Repository<User, Long> {
private Map<Long, User> database = new HashMap<>();
private long nextId = 1;
@Override
public void save(User entity) {
if (entity.getId() == null) {
entity.setId(nextId++);
}
database.put(entity.getId(), entity);
}
@Override
public User findById(Long id) {
return database.get(id);
}
@Override
public void delete(Long id) {
database.remove(id);
}
@Override
public List<User> findAll() {
return new ArrayList<>(database.values());
}
}
// Implementing generic interface with generic class
class GenericRepository<T, ID> implements Repository<T, ID> {
private Map<ID, T> database = new HashMap<>();
@Override
public void save(T entity) {
// Implementation would need reflection to get ID
System.out.println("Saving: " + entity);
}
@Override
public T findById(ID id) {
return database.get(id);
}
@Override
public void delete(ID id) {
database.remove(id);
}
@Override
public List<T> findAll() {
return new ArrayList<>(database.values());
}
}
// Generic interface implementation with bounds
class NumberMapper implements Mapper<Number, String> {
@Override
public String map(Number source) {
return source.toString();
}
@Override
public Number reverseMap(String target) {
try {
if (target.contains(".")) {
return Double.parseDouble(target);
} else {
return Long.parseLong(target);
}
} catch (NumberFormatException e) {
return 0;
}
}
}
// Supporting class
class User {
private Long id;
private String name;
public User(String name) {
this.name = name;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
}
// Interface implementation demo
public class GenericInterfaceDemo {
public static void main(String[] args) {
// Specific implementation
UserRepository userRepo = new UserRepository();
User user = new User("John Doe");
userRepo.save(user);
User found = userRepo.findById(1L);
System.out.println("Found user: " + found);
// Generic implementation
GenericRepository<String, Integer> stringRepo = new GenericRepository<>();
stringRepo.save("Test String");
// Mapper implementation
NumberMapper mapper = new NumberMapper();
String mapped = mapper.map(42);
System.out.println("Mapped number: " + mapped);
Number reversed = mapper.reverseMap("3.14");
System.out.println("Reversed string: " + reversed);
}
}
Type Erasure
Understanding Type Erasure
import java.util.*;
import java.lang.reflect.*;
public class TypeErasureDemo {
// Generic class that will undergo type erasure
public static class ErasureExample<T> {
private T value;
public ErasureExample(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
// This method demonstrates bridge methods
public void process(List<T> list) {
System.out.println("Processing list: " + list);
}
}
// Demonstrating what happens after type erasure
public static void demonstrateErasure() {
// At compile time, these are different types
ErasureExample<String> stringExample = new ErasureExample<>("Hello");
ErasureExample<Integer> integerExample = new ErasureExample<>(42);
// But after erasure, both become ErasureExample<Object>
System.out.println("String example class: " + stringExample.getClass());
System.out.println("Integer example class: " + integerExample.getClass());
System.out.println("Same class? " + (stringExample.getClass() == integerExample.getClass()));
// Reflection shows the raw type
Field[] fields = ErasureExample.class.getDeclaredFields();
for (Field field : fields) {
System.out.println("Field: " + field.getName() + ", Type: " + field.getType());
}
}
// Type erasure and method overloading
public static class OverloadingIssue {
// This won't compile - same erasure
// public void process(List<String> list) { }
// public void process(List<Integer> list) { }
// Workaround: use different method names or parameters
public void processStrings(List<String> list) {
System.out.println("Processing strings: " + list);
}
public void processIntegers(List<Integer> list) {
System.out.println("Processing integers: " + list);
}
}
// Reifiable types vs non-reifiable types
public static void reifiableTypes() {
// Reifiable types (type information available at runtime)
List<String> strings = new ArrayList<>();
System.out.println("List type: " + strings.getClass());
// Arrays are reifiable
String[] stringArray = new String[10];
System.out.println("Array type: " + stringArray.getClass());
// But generic type parameters are erased
System.out.println("List generic type: " + strings.getClass().getTypeParameters());
}
// Working with type tokens to preserve type information
public static class TypeToken<T> {
private final Class<T> type;
@SuppressWarnings("unchecked")
public TypeToken() {
this.type = (Class<T>) ((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0];
}
public Class<T> getType() {
return type;
}
}
// Using type tokens
public static <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
public static void main(String[] args) throws Exception {
System.out.println("=== Type Erasure Demonstration ===");
demonstrateErasure();
System.out.println("\n=== Reifiable Types ===");
reifiableTypes();
System.out.println("\n=== Type Tokens ===");
TypeToken<String> stringToken = new TypeToken<String>() {};
System.out.println("Type: " + stringToken.getType());
// Create instance using type token
String instance = createInstance(String.class);
System.out.println("Created instance: " + instance);
}
}
Overcoming Type Erasure Limitations
import java.util.*;
import java.lang.reflect.*;
public class OvercomingErasure {
// Technique 1: Pass Class<T> as parameter
public static class TypeSafeContainer<T> {
private T value;
private final Class<T> type;
public TypeSafeContainer(Class<T> type) {
this.type = type;
}
public TypeSafeContainer(Class<T> type, T value) {
this.type = type;
this.value = value;
}
public void setValue(T value) {
// Runtime type checking
if (value != null && !type.isInstance(value)) {
throw new IllegalArgumentException("Invalid type");
}
this.value = value;
}
public T getValue() {
return value;
}
public Class<T> getType() {
return type;
}
// Create array of the specific type
@SuppressWarnings("unchecked")
public T[] createArray(int size) {
return (T[]) Array.newInstance(type, size);
}
}
// Technique 2: Super Type Tokens
public abstract class SuperTypeToken<T> {
private final Type type;
protected SuperTypeToken() {
Type superclass = getClass().getGenericSuperclass();
if (superclass instanceof Class) {
throw new RuntimeException("Missing type parameter");
}
this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
// Technique 3: Type-safe heterogeneous container
public static class TypeSafeMap {
private Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> key, T value) {
map.put(key, value);
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> key) {
return (T) map.get(key);
}
}
// Technique 4: Runtime type checking in generic methods
public static <T> void checkType(T obj, Class<T> expectedType) {
if (obj != null && !expectedType.isInstance(obj)) {
throw new ClassCastException("Expected " + expectedType + " but got " + obj.getClass());
}
}
public static void main(String[] args) {
// Technique 1: Using Class<T>
TypeSafeContainer<String> stringContainer = new TypeSafeContainer<>(String.class, "Hello");
System.out.println("Container type: " + stringContainer.getType());
System.out.println("Container value: " + stringContainer.getValue());
String[] stringArray = stringContainer.createArray(5);
System.out.println("Created array of type: " + stringArray.getClass().getComponentType());
// Technique 3: Type-safe map
TypeSafeMap typeMap = new TypeSafeMap();
typeMap.put(String.class, "String value");
typeMap.put(Integer.class, 42);
String stringValue = typeMap.get(String.class);
Integer intValue = typeMap.get(Integer.class);
System.out.println("String value: " + stringValue);
System.out.println("Integer value: " + intValue);
// Technique 4: Runtime type checking
checkType("test", String.class);
// checkType(123, String.class); // Throws ClassCastException
}
}
Real-World Examples
Generic Data Structures
import java.util.*;
// Generic Stack implementation
public class GenericStack<T> {
private List<T> elements;
private int top;
public GenericStack() {
this(10); // Default capacity
}
public GenericStack(int capacity) {
this.elements = new ArrayList<>(capacity);
this.top = -1;
}
public void push(T element) {
elements.add(element);
top++;
}
public T pop() {
if (isEmpty()) {
throw new EmptyStackException();
}
T element = elements.get(top);
elements.remove(top);
top--;
return element;
}
public T peek() {
if (isEmpty()) {
throw new EmptyStackException();
}
return elements.get(top);
}
public boolean isEmpty() {
return top == -1;
}
public int size() {
return top + 1;
}
public void clear() {
elements.clear();
top = -1;
}
@Override
public String toString() {
return "Stack: " + elements;
}
}
// Generic Binary Tree implementation
public class BinaryTree<T extends Comparable<T>> {
private Node<T> root;
private static class Node<T> {
T data;
Node<T> left;
Node<T> right;
Node(T data) {
this.data = data;
}
}
public void insert(T data) {
root = insertRec(root, data);
}
private Node<T> insertRec(Node<T> node, T data) {
if (node == null) {
return new Node<>(data);
}
if (data.compareTo(node.data) < 0) {
node.left = insertRec(node.left, data);
} else if (data.compareTo(node.data) > 0) {
node.right = insertRec(node.right, data);
}
return node;
}
public boolean contains(T data) {
return containsRec(root, data);
}
private boolean containsRec(Node<T> node, T data) {
if (node == null) {
return false;
}
int cmp = data.compareTo(node.data);
if (cmp == 0) {
return true;
} else if (cmp < 0) {
return containsRec(node.left, data);
} else {
return containsRec(node.right, data);
}
}
public List<T> inOrderTraversal() {
List<T> result = new ArrayList<>();
inOrderRec(root, result);
return result;
}
private void inOrderRec(Node<T> node, List<T> result) {
if (node != null) {
inOrderRec(node.left, result);
result.add(node.data);
inOrderRec(node.right, result);
}
}
}
// Generic utility classes
public class MathUtils {
// Generic method for numerical operations
public static <T extends Number & Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
public static <T extends Number> double sum(Collection<T> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
public static <T extends Number> double average(Collection<T> numbers) {
if (numbers.isEmpty()) {
return 0.0;
}
return sum(numbers) / numbers.size();
}
}
// Real-world usage demonstration
public class GenericDataStructuresDemo {
public static void main(String[] args) {
// Stack demonstration
GenericStack<String> stringStack = new GenericStack<>();
stringStack.push("First");
stringStack.push("Second");
stringStack.push("Third");
System.out.println("Stack: " + stringStack);
System.out.println("Popped: " + stringStack.pop());
System.out.println("Stack after pop: " + stringStack);
// Binary tree demonstration
BinaryTree<Integer> tree = new BinaryTree<>();
tree.insert(50);
tree.insert(30);
tree.insert(70);
tree.insert(20);
tree.insert(40);
tree.insert(60);
tree.insert(80);
System.out.println("Tree contains 40: " + tree.contains(40));
System.out.println("Tree contains 90: " + tree.contains(90));
System.out.println("In-order traversal: " + tree.inOrderTraversal());
// Math utils demonstration
System.out.println("Max of 5 and 10: " + MathUtils.max(5, 10));
System.out.println("Max of 3.14 and 2.71: " + MathUtils.max(3.14, 2.71));
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
System.out.println("Sum: " + MathUtils.sum(numbers));
System.out.println("Average: " + MathUtils.average(numbers));
}
}
Generic Repository Pattern
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
// Base entity interface
interface Entity<ID> {
ID getId();
void setId(ID id);
}
// Generic repository interface
interface GenericRepository<T extends Entity<ID>, ID> {
T save(T entity);
Optional<T> findById(ID id);
List<T> findAll();
void deleteById(ID id);
boolean existsById(ID id);
long count();
}
// In-memory generic repository implementation
public class InMemoryRepository<T extends Entity<ID>, ID> implements GenericRepository<T, ID> {
private final Map<ID, T> storage = new HashMap<>();
private final AtomicLong sequence = new AtomicLong(1);
@Override
public T save(T entity) {
if (entity.getId() == null) {
// Auto-generate ID for Long types
if (entity.getId() instanceof Long) {
@SuppressWarnings("unchecked")
ID newId = (ID) Long.valueOf(sequence.getAndIncrement());
entity.setId(newId);
}
}
storage.put(entity.getId(), entity);
return entity;
}
@Override
public Optional<T> findById(ID id) {
return Optional.ofNullable(storage.get(id));
}
@Override
public List<T> findAll() {
return new ArrayList<>(storage.values());
}
@Override
public void deleteById(ID id) {
storage.remove(id);
}
@Override
public boolean existsById(ID id) {
return storage.containsKey(id);
}
@Override
public long count() {
return storage.size();
}
// Additional generic methods
public List<T> findByPredicate(java.util.function.Predicate<T> predicate) {
return storage.values().stream()
.filter(predicate)
.toList();
}
public <R> List<R> map(java.util.function.Function<T, R> mapper) {
return storage.values().stream()
.map(mapper)
.toList();
}
}
// Concrete entity implementations
class User implements Entity<Long> {
private Long id;
private String username;
private String email;
public User() {}
public User(String username, String email) {
this.username = username;
this.email = email;
}
@Override
public Long getId() { return id; }
@Override
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
@Override
public String toString() {
return String.format("User{id=%d, username='%s', email='%s'}", id, username, email);
}
}
class Product implements Entity<String> {
private String sku; // Use SKU as ID
private String name;
private double price;
public Product(String sku, String name, double price) {
this.sku = sku;
this.name = name;
this.price = price;
}
@Override
public String getId() { return sku; }
@Override
public void setId(String sku) { this.sku = sku; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
@Override
public String toString() {
return String.format("Product{sku='%s', name='%s', price=%.2f}", sku, name, price);
}
}
// Repository usage demonstration
public class RepositoryPatternDemo {
public static void main(String[] args) {
// User repository with Long ID
InMemoryRepository<User, Long> userRepository = new InMemoryRepository<>();
User user1 = new User("john_doe", "[email protected]");
User user2 = new User("jane_smith", "[email protected]");
userRepository.save(user1);
userRepository.save(user2);
System.out.println("All users:");
userRepository.findAll().forEach(System.out::println);
// Find by ID
Optional<User> foundUser = userRepository.findById(1L);
foundUser.ifPresent(user ->
System.out.println("Found user: " + user));
// Find by predicate
List<User> johns = userRepository.findByPredicate(
user -> user.getUsername().contains("john"));
System.out.println("Users with 'john' in username: " + johns);
// Map to usernames
List<String> usernames = userRepository.map(User::getUsername);
System.out.println("Usernames: " + usernames);
// Product repository with String ID
InMemoryRepository<Product, String> productRepository = new InMemoryRepository<>();
Product product1 = new Product("SKU001", "Laptop", 999.99);
Product product2 = new Product("SKU002", "Mouse", 29.99);
productRepository.save(product1);
productRepository.save(product2);
System.out.println("\nAll products:");
productRepository.findAll().forEach(System.out::println);
// Product-specific query
List<Product> expensiveProducts = productRepository.findByPredicate(
product -> product.getPrice() > 50.0);
System.out.println("Expensive products: " + expensiveProducts);
}
}
Best Practices
Generic Programming Guidelines
import java.util.*;
public class GenericBestPractices {
// 1. Use descriptive type parameter names
public static class Container<ELEMENT> { // Good
private ELEMENT value;
// ...
}
public static class Map<KEY, VALUE> { // Good
// ...
}
// Avoid single-letter names unless conventional (T, E, K, V, etc.)
// 2. Prefer generic types over raw types
public static void preferGenerics() {
// Good
List<String> strings = new ArrayList<>();
// Bad - raw type
@SuppressWarnings("rawtypes")
List rawList = new ArrayList();
}
// 3. Use bounded wildcards for maximum flexibility
public static class FlexibleCollections {
// Producer - use extends
public static double sumNumbers(Collection<? extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
// Consumer - use super
public static void addNumbers(Collection<? super Integer> numbers) {
numbers.add(1);
numbers.add(2);
numbers.add(3);
}
// Neither - use unbounded
public static void printAll(Collection<?> collection) {
collection.forEach(System.out::println);
}
}
// 4. Avoid using generic types in static contexts
public static class StaticContext<T> {
// This won't compile - cannot have static field of type T
// private static T staticField;
// But this is fine - the type parameter is on the method
public static <U> U firstElement(List<U> list) {
return list.isEmpty() ? null : list.get(0);
}
}
// 5. Use type inference where possible
public static void useTypeInference() {
// Good - diamond operator
List<String> names = new ArrayList<>();
// Good - type inference in methods
List<Integer> numbers = Arrays.asList(1, 2, 3);
// Good - local variable type inference (Java 10+)
var inferredList = new ArrayList<String>();
}
// 6. Handle type erasure appropriately
public static class ErasureAware<T> {
private final Class<T> type;
public ErasureAware(Class<T> type) {
this.type = type;
}
@SuppressWarnings("unchecked")
public T[] createArray(int size) {
return (T[]) java.lang.reflect.Array.newInstance(type, size);
}
public boolean isInstance(Object obj) {
return type.isInstance(obj);
}
}
// 7. Document generic type parameters
/**
* A generic container that holds a value of specified type.
*
* @param <T> the type of value held by this container
*/
public static class DocumentedContainer<T> {
private T value;
/**
* Sets the value held by this container.
*
* @param value the value to set, must not be null
*/
public void setValue(T value) {
this.value = Objects.requireNonNull(value, "Value cannot be null");
}
public T getValue() {
return value;
}
}
// 8. Use @SuppressWarnings judiciously
@SuppressWarnings("unchecked")
public static <T> T[] unsafeCast(Object[] array) {
// Only suppress warnings when you're sure it's safe
return (T[]) array;
}
// 9. Consider performance implications
public static class PerformanceConsiderations {
// Arrays generally have better performance than generic collections
// but lack type safety
// Use primitive specialized collections when possible
public void usePrimitiveCollections() {
IntSummaryStatistics stats = Arrays.asList(1, 2, 3, 4, 5)
.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println("Average: " + stats.getAverage());
}
}
// 10. Test generic code thoroughly
public static class GenericTestExamples {
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// Test with different types
public static void testMax() {
assert max(5, 10) == 10;
assert max("apple", "banana").equals("banana");
assert max(3.14, 2.71) == 3.14;
}
}
public static void main(String[] args) {
// Demonstrate best practices
FlexibleCollections.addNumbers(new ArrayList<Number>());
List<Number> numbers = Arrays.asList(1, 2.5, 3L);
double sum = FlexibleCollections.sumNumbers(numbers);
System.out.println("Sum: " + sum);
// Type erasure awareness
ErasureAware<String> aware = new ErasureAware<>(String.class);
String[] array = aware.createArray(5);
System.out.println("Array type: " + array.getClass().getComponentType());
// Performance considerations
new PerformanceConsiderations().usePrimitiveCollections();
// Testing
GenericTestExamples.testMax();
}
}
Common Generic Patterns
import java.util.*;
import java.util.function.*;
public class CommonGenericPatterns {
// Pattern 1: Builder pattern with generics
public static class GenericBuilder<T> {
private final Supplier<T> instantiator;
private List<Consumer<T>> modifiers = new ArrayList<>();
public GenericBuilder(Supplier<T> instantiator) {
this.instantiator = instantiator;
}
public static <T> GenericBuilder<T> of(Supplier<T> instantiator) {
return new GenericBuilder<>(instantiator);
}
public <U> GenericBuilder<T> with(BiConsumer<T, U> consumer, U value) {
Consumer<T> c = instance -> consumer.accept(instance, value);
modifiers.add(c);
return this;
}
public T build() {
T value = instantiator.get();
modifiers.forEach(modifier -> modifier.accept(value));
modifiers.clear();
return value;
}
}
// Pattern 2: Factory pattern with generics
public interface Factory<T> {
T create();
}
public static class GenericFactory {
private static final Map<Class<?>, Factory<?>> factories = new HashMap<>();
public static <T> void registerFactory(Class<T> type, Factory<T> factory) {
factories.put(type, factory);
}
@SuppressWarnings("unchecked")
public static <T> T create(Class<T> type) {
Factory<T> factory = (Factory<T>) factories.get(type);
if (factory == null) {
throw new IllegalArgumentException("No factory registered for " + type);
}
return factory.create();
}
}
// Pattern 3: Strategy pattern with generics
public interface ValidationStrategy<T> {
boolean isValid(T value);
}
public static class Validator<T> {
private final List<ValidationStrategy<T>> strategies = new ArrayList<>();
public Validator<T> addStrategy(ValidationStrategy<T> strategy) {
strategies.add(strategy);
return this;
}
public boolean validate(T value) {
return strategies.stream().allMatch(strategy -> strategy.isValid(value));
}
}
// Pattern 4: Repository pattern (as shown earlier)
// Pattern 5: Wrapper/Decorator pattern with generics
public static class Decorator<T> {
private final T wrapped;
private final List<Function<T, T>> decorators = new ArrayList<>();
public Decorator(T wrapped) {
this.wrapped = wrapped;
}
public Decorator<T> with(Function<T, T> decorator) {
decorators.add(decorator);
return this;
}
public T decorate() {
T result = wrapped;
for (Function<T, T> decorator : decorators) {
result = decorator.apply(result);
}
return result;
}
}
// Example usage classes
public static class Person {
private String name;
private int age;
public Person() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return String.format("Person{name='%s', age=%d}", name, age);
}
}
public static class Product {
private String name;
private double price;
public Product() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
@Override
public String toString() {
return String.format("Product{name='%s', price=%.2f}", name, price);
}
}
// Pattern demonstration
public static void main(String[] args) {
// Builder pattern
Person person = GenericBuilder.of(Person::new)
.with(Person::setName, "John Doe")
.with(Person::setAge, 30)
.build();
System.out.println("Built person: " + person);
// Factory pattern
GenericFactory.registerFactory(String.class, () -> "Default String");
GenericFactory.registerFactory(Integer.class, () -> 42);
String str = GenericFactory.create(String.class);
Integer num = GenericFactory.create(Integer.class);
System.out.println("Factory created: " + str + ", " + num);
// Strategy pattern
Validator<String> emailValidator = new Validator<String>()
.addStrategy(s -> s.contains("@"))
.addStrategy(s -> s.length() > 5);
System.out.println("Valid email? " + emailValidator.validate("[email protected]"));
System.out.println("Invalid email? " + emailValidator.validate("test"));
// Decorator pattern
String original = "hello";
String decorated = new Decorator<>(original)
.with(String::toUpperCase)
.with(s -> s + "!")
.with(s -> "*** " + s + " ***")
.decorate();
System.out.println("Decorated: " + decorated);
}
}
Summary
Key Points:
- Generics provide type safety and eliminate casting
- Use bounded type parameters (
<T extends Class>) for constraints - Follow PECS principle: Producer Extends, Consumer Super
- Type erasure removes generic type information at runtime
- Wildcards provide flexibility in method signatures
Common Type Parameter Conventions:
T- TypeE- Element (collections)K- Key (maps)V- Value (maps)N- NumberS,U,V- Additional types
When to Use Generics:
- Creating reusable data structures
- Implementing type-safe collections
- Building flexible APIs
- Writing type-safe algorithms
Generics are a powerful feature that, when used properly, can significantly improve code quality, type safety, and reusability while reducing boilerplate code and runtime errors.