Table of Contents
- What are Generics?
- Why Use Generics?
- Generic Classes
- Generic Methods
- Generic Interfaces
- Type Parameters Naming Conventions
- Bounded Type Parameters
- Wildcards
- Type Erasure
- Best Practices
- Common Examples
- Complete Examples
What are Generics?
Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. They provide stronger type checks at compile time and eliminate the need for casting.
Before Generics (Java 1.4 and earlier):
List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); // Requires explicit casting With Generics (Java 5+):
List<String> list = new ArrayList<>(); list.add("hello"); String s = list.get(0); // No casting needed Why Use Generics?
Benefits:
- Type Safety: Compile-time type checking
- Eliminates Casting: No need for explicit type casting
- Code Reusability: Write once, use with different types
- Better Readability: Clear about what type of objects are stored
Problem Without Generics:
import java.util.*; public class WithoutGenerics { public static void main(String[] args) { // Without generics - can add any type List list = new ArrayList(); list.add("String"); list.add(123); // Integer list.add(new Date()); // Date object // Runtime ClassCastException possible String str = (String) list.get(1); // Throws ClassCastException! } } Solution With Generics:
import java.util.*; public class WithGenerics { public static void main(String[] args) { // With generics - type safe List<String> list = new ArrayList<>(); list.add("String"); // list.add(123); // Compile-time error! // list.add(new Date()); // Compile-time error! String str = list.get(0); // No casting needed } } Generic Classes
Basic Generic Class:
// Generic class with single type parameter class Box<T> { private T content; public void setContent(T content) { this.content = content; } public T getContent() { return content; } @Override public String toString() { return "Box containing: " + content; } } public class GenericClassExample { public static void main(String[] args) { // Box for String Box<String> stringBox = new Box<>(); stringBox.setContent("Hello Generics!"); System.out.println(stringBox); // Box for Integer Box<Integer> integerBox = new Box<>(); integerBox.setContent(42); System.out.println(integerBox); // Box for custom object Box<Date> dateBox = new Box<>(); dateBox.setContent(new Date()); System.out.println(dateBox); } } Output:
Box containing: Hello Generics! Box containing: 42 Box containing: Wed Jan 10 14:30:00 IST 2024
Multiple Type Parameters:
// Generic class with multiple type parameters 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; } @Override public String toString() { return "Pair{key=" + key + ", value=" + value + "}"; } } public class MultipleTypeParameters { public static void main(String[] args) { // String-Integer pair Pair<String, Integer> nameAge = new Pair<>("Alice", 25); System.out.println(nameAge); // Integer-String pair Pair<Integer, String> idName = new Pair<>(101, "Bob"); System.out.println(idName); // String-String pair Pair<String, String> countryCapital = new Pair<>("France", "Paris"); System.out.println(countryCapital); // Custom types Pair<String, List<Integer>> scores = new Pair<>("Math", Arrays.asList(85, 90, 78)); System.out.println(scores); } } Output:
Pair{key=Alice, value=25} Pair{key=101, value=Bob} Pair{key=France, value=Paris} Pair{key=Math, value=[85, 90, 78]} Generic Methods
Basic Generic Methods:
public class GenericMethods { // Generic method in non-generic class public static <T> void printArray(T[] array) { for (T element : array) { System.out.print(element + " "); } System.out.println(); } // Generic method with multiple type parameters public static <T, U> void printPair(T first, U second) { System.out.println("First: " + first + ", Second: " + second); } // Generic method returning generic type public static <T> T getFirstElement(T[] array) { if (array == null || array.length == 0) { return null; } return array[0]; } public static void main(String[] args) { // Using generic methods with different types Integer[] intArray = {1, 2, 3, 4, 5}; String[] strArray = {"A", "B", "C", "D"}; Double[] doubleArray = {1.1, 2.2, 3.3}; System.out.print("Integer array: "); printArray(intArray); System.out.print("String array: "); printArray(strArray); System.out.print("Double array: "); printArray(doubleArray); // Using multiple type parameters printPair("Hello", 42); printPair(3.14, "Pi"); printPair(true, new Date()); // Getting first element System.out.println("First string: " + getFirstElement(strArray)); System.out.println("First integer: " + getFirstElement(intArray)); } } Output:
Integer array: 1 2 3 4 5 String array: A B C D Double array: 1.1 2.2 3.3 First: Hello, Second: 42 First: 3.14, Second: Pi First: true, Second: Wed Jan 10 14:30:00 IST 2024 First string: A First integer: 1
Generic Interfaces
Generic Interface Examples:
// Generic interface interface Repository<T> { void save(T entity); T findById(int id); List<T> findAll(); void delete(int id); } // Implementation for User class UserRepository implements Repository<User> { private List<User> users = new ArrayList<>(); private int nextId = 1; @Override public void save(User user) { user.setId(nextId++); users.add(user); } @Override public User findById(int id) { return users.stream() .filter(user -> user.getId() == id) .findFirst() .orElse(null); } @Override public List<User> findAll() { return new ArrayList<>(users); } @Override public void delete(int id) { users.removeIf(user -> user.getId() == id); } } // Implementation for Product class ProductRepository implements Repository<Product> { private Map<Integer, Product> products = new HashMap<>(); private int nextId = 1; @Override public void save(Product product) { product.setId(nextId); products.put(nextId++, product); } @Override public Product findById(int id) { return products.get(id); } @Override public List<Product> findAll() { return new ArrayList<>(products.values()); } @Override public void delete(int id) { products.remove(id); } } // Supporting classes class User { private int id; private String name; private String email; public User(String name, String email) { this.name = name; this.email = email; } // Getters and setters public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public String getEmail() { return email; } @Override public String toString() { return String.format("User{id=%d, name='%s', email='%s'}", id, name, email); } } class Product { private int id; private String name; private double price; public Product(String name, double price) { this.name = name; this.price = price; } // Getters and setters public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public double getPrice() { return price; } @Override public String toString() { return String.format("Product{id=%d, name='%s', price=$%.2f}", id, name, price); } } public class GenericInterfaceExample { public static void main(String[] args) { // User repository Repository<User> userRepo = new UserRepository(); userRepo.save(new User("Alice", "[email protected]")); userRepo.save(new User("Bob", "[email protected]")); System.out.println("All users:"); userRepo.findAll().forEach(System.out::println); // Product repository Repository<Product> productRepo = new ProductRepository(); productRepo.save(new Product("Laptop", 999.99)); productRepo.save(new Product("Mouse", 25.99)); System.out.println("\nAll products:"); productRepo.findAll().forEach(System.out::println); // Find by ID System.out.println("\nUser with ID 1: " + userRepo.findById(1)); System.out.println("Product with ID 2: " + productRepo.findById(2)); } } Output:
All users: User{id=1, name='Alice', email='[email protected]'} User{id=2, name='Bob', email='[email protected]'} All products: Product{id=1, name='Laptop', price=$999.99} Product{id=2, name='Mouse', price=$25.99} User with ID 1: User{id=1, name='Alice', email='[email protected]'} Product with ID 2: Product{id=2, name='Mouse', price=$25.99} Type Parameters Naming Conventions
Standard Naming Conventions:
- E - Element (used extensively by Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S, U, V - 2nd, 3rd, 4th types
Examples:
// Following naming conventions class Container<T> { } // Single type class Pair<K, V> { } // Key-Value pair class Calculator<N extends Number> { } // Number type class Processor<E> { } // Element type // Self-referential generic type class Node<T> { private T data; private Node<T> next; // Same type parameter public Node(T data) { this.data = data; } public T getData() { return data; } public Node<T> getNext() { return next; } public void setNext(Node<T> next) { this.next = next; } } Bounded Type Parameters
Upper Bounded Type Parameters:
import java.util.*; // Bounded type parameter - only accepts Number and its subclasses class Statistics<T extends Number> { private List<T> numbers; public Statistics() { numbers = new ArrayList<>(); } public void addNumber(T number) { numbers.add(number); } public double getAverage() { if (numbers.isEmpty()) { return 0.0; } double sum = 0.0; for (T number : numbers) { sum += number.doubleValue(); // Can call Number methods } return sum / numbers.size(); } public T getMax() { if (numbers.isEmpty()) { return null; } T max = numbers.get(0); for (T number : numbers) { if (number.doubleValue() > max.doubleValue()) { max = number; } } return max; } public List<T> getNumbers() { return new ArrayList<>(numbers); } } public class BoundedTypesExample { public static void main(String[] args) { // Works with Integer (subclass of Number) Statistics<Integer> intStats = new Statistics<>(); intStats.addNumber(10); intStats.addNumber(20); intStats.addNumber(30); System.out.println("Integer stats: " + intStats.getNumbers()); System.out.println("Average: " + intStats.getAverage()); System.out.println("Max: " + intStats.getMax()); // Works with Double (subclass of Number) Statistics<Double> doubleStats = new Statistics<>(); doubleStats.addNumber(15.5); doubleStats.addNumber(25.7); doubleStats.addNumber(35.2); System.out.println("\nDouble stats: " + doubleStats.getNumbers()); System.out.println("Average: " + doubleStats.getAverage()); System.out.println("Max: " + doubleStats.getMax()); // Won't compile - String is not a subclass of Number // Statistics<String> stringStats = new Statistics<>(); // Compile error! } } Output:
Integer stats: [10, 20, 30] Average: 20.0 Max: 30 Double stats: [15.5, 25.7, 35.2] Average: 25.466666666666665 Max: 35.2
Multiple Bounds:
interface Loggable { void log(); } interface Serializable { void serialize(); } class Document implements Loggable, Serializable { private String content; public Document(String content) { this.content = content; } @Override public void log() { System.out.println("Logging document: " + content); } @Override public void serialize() { System.out.println("Serializing document: " + content); } @Override public String toString() { return "Document: " + content; } } // Multiple bounds - T must implement both interfaces class Processor<T extends Loggable & Serializable> { private T item; public Processor(T item) { this.item = item; } public void process() { item.log(); item.serialize(); System.out.println("Processing: " + item); } } public class MultipleBoundsExample { public static void main(String[] args) { Document doc = new Document("Important data"); Processor<Document> processor = new Processor<>(doc); processor.process(); } } Output:
Logging document: Important data Serializing document: Important data Processing: Document: Important data
Wildcards
Upper Bounded Wildcards:
import java.util.*; public class UpperBoundedWildcards { // Accepts List of any type that is Number or subclass of Number public static double sumOfList(List<? extends Number> list) { double sum = 0.0; for (Number number : list) { sum += number.doubleValue(); } return sum; } public static void main(String[] args) { List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5); List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3, 4.4, 5.5); List<Float> floats = Arrays.asList(1.1f, 2.2f, 3.3f); System.out.println("Sum of integers: " + sumOfList(integers)); System.out.println("Sum of doubles: " + sumOfList(doubles)); System.out.println("Sum of floats: " + sumOfList(floats)); // Won't compile - String is not a Number // List<String> strings = Arrays.asList("A", "B", "C"); // System.out.println(sumOfList(strings)); // Compile error! } } Output:
Sum of integers: 15.0 Sum of doubles: 16.5 Sum of floats: 6.6000001430511475
Lower Bounded Wildcards:
import java.util.*; public class LowerBoundedWildcards { // Accepts List of Integer or superclasses of Integer public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 5; i++) { list.add(i); } } public static void main(String[] args) { List<Integer> integerList = new ArrayList<>(); List<Number> numberList = new ArrayList<>(); List<Object> objectList = new ArrayList<>(); addNumbers(integerList); addNumbers(numberList); addNumbers(objectList); System.out.println("Integer list: " + integerList); System.out.println("Number list: " + numberList); System.out.println("Object list: " + objectList); // Won't compile - String is not a superclass of Integer // List<String> stringList = new ArrayList<>(); // addNumbers(stringList); // Compile error! } } Output:
Integer list: [1, 2, 3, 4, 5] Number list: [1, 2, 3, 4, 5] Object list: [1, 2, 3, 4, 5]
Unbounded Wildcards:
import java.util.*; public class UnboundedWildcards { // Accepts List of any type public static void printList(List<?> list) { for (Object element : list) { System.out.print(element + " "); } System.out.println(); } public static int countElements(List<?> list) { return list.size(); } public static void main(String[] args) { List<String> strings = Arrays.asList("A", "B", "C"); List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5); List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3); System.out.print("Strings: "); printList(strings); System.out.print("Integers: "); printList(integers); System.out.print("Doubles: "); printList(doubles); System.out.println("Count of strings: " + countElements(strings)); System.out.println("Count of integers: " + countElements(integers)); System.out.println("Count of doubles: " + countElements(doubles)); } } Output:
Strings: A B C Integers: 1 2 3 4 5 Doubles: 1.1 2.2 3.3 Count of strings: 3 Count of integers: 5 Count of doubles: 3
Type Erasure
How Generics Work at Runtime:
import java.util.*; import java.lang.reflect.*; public class TypeErasureExample { public static void main(String[] args) { // At compile time - different types List<String> stringList = new ArrayList<>(); List<Integer> integerList = new ArrayList<>(); // At runtime - both are just List (type erasure) System.out.println("stringList class: " + stringList.getClass()); System.out.println("integerList class: " + integerList.getClass()); System.out.println("Same class? " + (stringList.getClass() == integerList.getClass())); // Type information is not available at runtime try { Method addMethod = ArrayList.class.getMethod("add", Object.class); System.out.println("Method parameter type: " + addMethod.getParameterTypes()[0]); } catch (Exception e) { e.printStackTrace(); } } } Output:
stringList class: class java.util.ArrayList integerList class: class java.util.ArrayList Same class? true Method parameter type: class java.lang.Object
Best Practices
1. Use Generic Types Instead of Raw Types:
// ✅ Good - type safe List<String> names = new ArrayList<>(); Map<String, Integer> scores = new HashMap<>(); // ❌ Bad - raw types List names = new ArrayList(); // Raw type warning Map scores = new HashMap(); // Raw type warning
2. Prefer Generic Methods:
// ✅ Good - generic method public static <T> T getFirst(List<T> list) { return list.isEmpty() ? null : list.get(0); } // ❌ Bad - uses raw types public static Object getFirstRaw(List list) { return list.isEmpty() ? null : list.get(0); } 3. Use Bounded Wildcards for Maximum Flexibility:
// ✅ Good - accepts any List of Number or subclass public static double sum(List<? extends Number> numbers) { return numbers.stream().mapToDouble(Number::doubleValue).sum(); } // ❌ Less flexible - only accepts exact List<Number> public static double sumExact(List<Number> numbers) { return numbers.stream().mapToDouble(Number::doubleValue).sum(); } 4. Avoid Using Generic Array Creation:
// ❌ Bad - generic array creation // T[] array = new T[10]; // Compile error! // ✅ Good - use ArrayList or other collection List<T> list = new ArrayList<>(); // ✅ Alternative - use Array.newInstance with Class<T> public static <T> T[] createArray(Class<T> clazz, int size) { @SuppressWarnings("unchecked") T[] array = (T[]) Array.newInstance(clazz, size); return array; } Common Examples
1. Generic Stack Implementation:
import java.util.*; class GenericStack<T> { private List<T> elements; public GenericStack() { elements = new ArrayList<>(); } public void push(T element) { elements.add(element); } public T pop() { if (isEmpty()) { throw new EmptyStackException(); } return elements.remove(elements.size() - 1); } public T peek() { if (isEmpty()) { throw new EmptyStackException(); } return elements.get(elements.size() - 1); } public boolean isEmpty() { return elements.isEmpty(); } public int size() { return elements.size(); } @Override public String toString() { return "Stack: " + elements; } } public class StackExample { public static void main(String[] args) { // String stack GenericStack<String> stringStack = new GenericStack<>(); stringStack.push("First"); stringStack.push("Second"); stringStack.push("Third"); System.out.println(stringStack); System.out.println("Popped: " + stringStack.pop()); System.out.println("Top: " + stringStack.peek()); System.out.println(stringStack); // Integer stack GenericStack<Integer> intStack = new GenericStack<>(); intStack.push(10); intStack.push(20); intStack.push(30); System.out.println("\n" + intStack); System.out.println("Popped: " + intStack.pop()); System.out.println("Size: " + intStack.size()); } } Output:
Stack: [First, Second, Third] Popped: Third Top: Second Stack: [First, Second] Stack: [10, 20, 30] Popped: 30 Size: 2
2. Generic Utility Methods:
import java.util.*; public class GenericUtilities { // Swap elements in an array public static <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; array[j] = temp; } // Find maximum element in collection public static <T extends Comparable<T>> T findMax(Collection<T> collection) { if (collection.isEmpty()) { throw new IllegalArgumentException("Collection is empty"); } T max = null; for (T element : collection) { if (max == null || element.compareTo(max) > 0) { max = element; } } return max; } // Convert array to list public static <T> List<T> arrayToList(T[] array) { return new ArrayList<>(Arrays.asList(array)); } // Check if array contains element public static <T> boolean contains(T[] array, T element) { for (T item : array) { if (Objects.equals(item, element)) { return true; } } return false; } public static void main(String[] args) { // Swap test Integer[] numbers = {1, 2, 3, 4, 5}; System.out.println("Before swap: " + Arrays.toString(numbers)); swap(numbers, 1, 3); System.out.println("After swap: " + Arrays.toString(numbers)); // Find max test List<Integer> numberList = Arrays.asList(5, 2, 8, 1, 9, 3); System.out.println("Max number: " + findMax(numberList)); List<String> stringList = Arrays.asList("apple", "zebra", "banana", "cherry"); System.out.println("Max string: " + findMax(stringList)); // Array to list test String[] stringArray = {"A", "B", "C"}; List<String> convertedList = arrayToList(stringArray); System.out.println("Converted list: " + convertedList); // Contains test System.out.println("Contains 'B': " + contains(stringArray, "B")); System.out.println("Contains 'X': " + contains(stringArray, "X")); } } Output:
Before swap: [1, 2, 3, 4, 5] After swap: [1, 4, 3, 2, 5] Max number: 9 Max string: zebra Converted list: [A, B, C] Contains 'B': true Contains 'X': false
Complete Examples
Complete Working Example:
import java.util.*; import java.util.function.Predicate; public class GenericsCompleteExample { public static void main(String[] args) { System.out.println("=== Generics Complete Example ===\n"); // Example 1: Generic Container genericContainerExample(); // Example 2: Generic Repository Pattern genericRepositoryExample(); // Example 3: Generic Algorithms genericAlgorithmsExample(); // Example 4: Advanced - Generic Builder genericBuilderExample(); } public static void genericContainerExample() { System.out.println("1. Generic Container Example:"); // Generic container for different types Container<String> stringContainer = new Container<>("Hello World"); Container<Integer> intContainer = new Container<>(42); Container<Date> dateContainer = new Container<>(new Date()); System.out.println("String container: " + stringContainer); System.out.println("Integer container: " + intContainer); System.out.println("Date container: " + dateContainer); // Generic pair Pair<String, Integer> nameAge = new Pair<>("Alice", 25); Pair<Integer, String> idName = new Pair<>(101, "Bob"); System.out.println("Name-Age pair: " + nameAge); System.out.println("ID-Name pair: " + idName); System.out.println(); } public static void genericRepositoryExample() { System.out.println("2. Generic Repository Pattern:"); // User repository CrudRepository<User> userRepo = new InMemoryCrudRepository<>(); userRepo.save(new User(1, "Alice", "[email protected]")); userRepo.save(new User(2, "Bob", "[email protected]")); userRepo.save(new User(3, "Charlie", "[email protected]")); System.out.println("All users:"); userRepo.findAll().forEach(System.out::println); System.out.println("User with ID 2: " + userRepo.findById(2)); // Product repository CrudRepository<Product> productRepo = new InMemoryCrudRepository<>(); productRepo.save(new Product(1, "Laptop", 999.99)); productRepo.save(new Product(2, "Mouse", 25.99)); System.out.println("\nAll products:"); productRepo.findAll().forEach(System.out::println); System.out.println(); } public static void genericAlgorithmsExample() { System.out.println("3. Generic Algorithms Example:"); // Filter example List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> evenNumbers = filter(numbers, n -> n % 2 == 0); System.out.println("Even numbers: " + evenNumbers); List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve"); List<String> longNames = filter(names, name -> name.length() > 4); System.out.println("Long names: " + longNames); // Map example List<String> upperCaseNames = map(names, String::toUpperCase); System.out.println("Uppercase names: " + upperCaseNames); List<Integer> squaredNumbers = map(numbers, n -> n * n); System.out.println("Squared numbers: " + squaredNumbers); System.out.println(); } public static void genericBuilderExample() { System.out.println("4. Generic Builder Example:"); // Build different types of objects Person person = new PersonBuilder() .withName("John Doe") .withAge(30) .withEmail("[email protected]") .build(); Product product = new ProductBuilder() .withName("Smartphone") .withPrice(699.99) .withCategory("Electronics") .build(); System.out.println("Built person: " + person); System.out.println("Built product: " + product); } // Generic filter method public static <T> List<T> filter(List<T> list, Predicate<T> predicate) { List<T> result = new ArrayList<>(); for (T item : list) { if (predicate.test(item)) { result.add(item); } } return result; } // Generic map method public static <T, R> List<R> map(List<T> list, java.util.function.Function<T, R> mapper) { List<R> result = new ArrayList<>(); for (T item : list) { result.add(mapper.apply(item)); } return result; } } // Supporting generic classes and interfaces // 1. Generic Container class Container<T> { private T value; public Container(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } @Override public String toString() { return "Container{" + value + "}"; } } // 2. Generic Pair 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; } @Override public String toString() { return "Pair{" + key + "=" + value + "}"; } } // 3. Generic Repository interface CrudRepository<T> { void save(T entity); T findById(int id); List<T> findAll(); void delete(int id); } class InMemoryCrudRepository<T> implements CrudRepository<T> { private Map<Integer, T> storage = new HashMap<>(); private int nextId = 1; @Override public void save(T entity) { // In real implementation, we'd use reflection to set ID // For demo, we'll assume entity has setId method storage.put(nextId++, entity); } @Override public T findById(int id) { return storage.get(id); } @Override public List<T> findAll() { return new ArrayList<>(storage.values()); } @Override public void delete(int id) { storage.remove(id); } } // 4. Entity classes class User { private int id; private String name; private String email; public User(int id, String name, String email) { this.id = id; this.name = name; this.email = email; } // Getters public int getId() { return id; } public String getName() { return name; } public String getEmail() { return email; } @Override public String toString() { return String.format("User{id=%d, name='%s', email='%s'}", id, name, email); } } class Product { private int id; private String name; private double price; private String category; public Product(int id, String name, double price) { this.id = id; this.name = name; this.price = price; } public Product(int id, String name, double price, String category) { this.id = id; this.name = name; this.price = price; this.category = category; } // Getters public int getId() { return id; } public String getName() { return name; } public double getPrice() { return price; } public String getCategory() { return category; } @Override public String toString() { return String.format("Product{id=%d, name='%s', price=$%.2f, category='%s'}", id, name, price, category); } } // 5. Generic Builder pattern class Person { private String name; private int age; private String email; private Person(String name, int age, String email) { this.name = name; this.age = age; this.email = email; } @Override public String toString() { return String.format("Person{name='%s', age=%d, email='%s'}", name, age, email); } // Builder static class PersonBuilder { private String name; private int age; private String email; public PersonBuilder withName(String name) { this.name = name; return this; } public PersonBuilder withAge(int age) { this.age = age; return this; } public PersonBuilder withEmail(String email) { this.email = email; return this; } public Person build() { return new Person(name, age, email); } } } class ProductBuilder { private String name; private double price; private String category; public ProductBuilder withName(String name) { this.name = name; return this; } public ProductBuilder withPrice(double price) { this.price = price; return this; } public ProductBuilder withCategory(String category) { this.category = category; return this; } public Product build() { return new Product(0, name, price, category); // ID would be set by repository } } Expected Output:
=== Generics Complete Example === 1. Generic Container Example: String container: Container{Hello World} Integer container: Container{42} Date container: Container{Wed Jan 10 14:30:00 IST 2024} Name-Age pair: Pair{Alice=25} ID-Name pair: Pair{101=Bob} 2. Generic Repository Pattern: All users: User{id=1, name='Alice', email='[email protected]'} User{id=2, name='Bob', email='[email protected]'} User{id=3, name='Charlie', email='[email protected]'} User with ID 2: User{id=2, name='Bob', email='[email protected]'} All products: Product{id=1, name='Laptop', price=$999.99, category='null'} Product{id=2, name='Mouse', price=$25.99, category='null'} 3. Generic Algorithms Example: Even numbers: [2, 4, 6, 8, 10] Long names: [Alice, Charlie, David] Uppercase names: [ALICE, BOB, CHARLIE, DAVID, EVE] Squared numbers: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] 4. Generic Builder Example: Built person: Person{name='John Doe', age=30, email='[email protected]'} Built product: Product{id=0, name='Smartphone', price=$699.99, category='Electronics'} Key Takeaways
- Type Safety: Generics provide compile-time type checking
- Code Reusability: Write generic code that works with different types
- No Casting: Eliminate explicit type casting
- Bounded Types: Restrict types that can be used with generics
- Wildcards: Provide flexibility with
? extendsand? super - Type Erasure: Generic type information is removed at runtime
- Best Practices: Use generic types, avoid raw types, prefer bounded wildcards
Generics are fundamental to modern Java programming and are extensively used in Collections Framework, making code safer, more readable, and reusable.