google-site-verification: google61fe8ba583a51912.html
Generic Classes in Java: Complete Guide

Table of Contents

  1. Introduction to Generics
  2. Generic Class Syntax
  3. Type Parameters and Bounds
  4. Generic Methods
  5. Wildcards
  6. Inheritance with Generics
  7. Type Erasure
  8. Real-World Examples
  9. 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:

  1. Generics provide type safety and eliminate casting
  2. Use bounded type parameters (<T extends Class>) for constraints
  3. Follow PECS principle: Producer Extends, Consumer Super
  4. Type erasure removes generic type information at runtime
  5. Wildcards provide flexibility in method signatures

Common Type Parameter Conventions:

  • T - Type
  • E - Element (collections)
  • K - Key (maps)
  • V - Value (maps)
  • N - Number
  • S, 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper