Java Generics - Complete Tutorial
Master Java Generics: Learn type parameters, generic classes and methods, bounded types, wildcards, type erasure with practical examples and best practices for type-safe code.
Type Safety
Compile-time checking
Code Reuse
Single implementation
Bounded Types
Restrict type parameters
Wildcards
Flexible typing
1. Introduction to Java Generics
Generics in Java are a compile-time type safety feature that allows you to write parameterized types. Introduced in Java 5, generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods.
Why Use Generics?
- Type Safety: Catch type errors at compile-time
- Code Reuse: Write once, use with different types
- No Casting Needed: Eliminate explicit type casting
- Better Readability: Clear type intentions in code
- Algorithm Reuse: Implement algorithms independently of data types
- Collection Framework: Foundation of type-safe collections
Key Concepts
- Type Parameters:
<T>,<E>,<K,V> - Generic Classes: Classes with type parameters
- Generic Methods: Methods with their own type parameters
- Bounded Types:
<T extends Number> - Wildcards:
?,? extends T,? super T - Type Erasure: How generics work at runtime
Before vs After Generics
// Pre-Java 5 - Raw types
List list = new ArrayList();
list.add("Hello");
list.add(123); // No compile error!
String s = (String) list.get(0); // Cast needed
Integer i = (Integer) list.get(1); // Runtime ClassCastException!
// Java 5+ - Type safety
List list = new ArrayList<>();
list.add("Hello");
list.add(123); // Compile error!
String s = list.get(0); // No cast needed
// Integer i = list.get(1); // Won't compile
2. Generic Classes
A generic class is a class that can work with different types. The type is specified as a parameter when the class is instantiated.
// Simple generic class with single type parameter
public class Box {
private T content;
public Box() {
this.content = null;
}
public Box(T content) {
this.content = content;
}
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
@Override
public String toString() {
return "Box containing: " + content;
}
}
// Generic class with multiple type parameters
public class Pair {
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 + " => " + value + "]";
}
}
// Test generic classes
public class GenericClassExample {
public static void main(String[] args) {
// Box with String
Box stringBox = new Box<>("Hello Generics!");
System.out.println(stringBox);
// Box with Integer
Box intBox = new Box<>(42);
System.out.println(intBox);
// Box with custom object
Box personBox = new Box<>(new Person("Alice", 30));
System.out.println(personBox);
// Pair example
Pair nameAge = new Pair<>("Bob", 25);
System.out.println(nameAge);
Pair idName = new Pair<>(101, "Charlie");
System.out.println(idName);
// Generic class inheritance
NumberBox doubleBox = new NumberBox<>(3.14);
System.out.println("Double box: " + doubleBox);
}
}
// Generic class extending another class
class NumberBox extends Box {
public NumberBox(T content) {
super(content);
}
public double square() {
return getContent().doubleValue() * getContent().doubleValue();
}
}
// Simple Person class for example
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
| Type Parameter | Common Meaning | Example | Where Used |
|---|---|---|---|
T |
Type | Box<T> |
General purpose |
E |
Element | List<E> |
Collections |
K |
Key | Map<K,V> |
Maps |
V |
Value | Map<K,V> |
Maps |
N |
Number | Calculator<N> |
Numerics |
3. Generic Methods
Generic methods are methods that introduce their own type parameters. This is useful when you want to create a method that can work with different types independently of the class's type parameters.
import java.util.Arrays;
import java.util.List;
public class GenericMethodExample {
// Simple generic method
public static void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
// Generic method returning a value
public static T getMiddle(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[array.length / 2];
}
// Generic method with multiple type parameters
public static void printPair(K key, V value) {
System.out.println(key + " => " + value);
}
// Generic method in non-generic class
public static > T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// Generic method with wildcard parameter
public static void printList(List> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
// Generic method using type inference
public static T[] swap(T[] array, int i, int j) {
if (i < 0 || i >= array.length || j < 0 || j >= array.length) {
throw new IllegalArgumentException("Invalid indices");
}
T temp = array[i];
array[i] = array[j];
array[j] = temp;
return array;
}
public static void main(String[] args) {
// Test printArray with different types
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"Hello", "World", "Java"};
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);
// Test getMiddle
System.out.println("Middle of int array: " + getMiddle(intArray));
System.out.println("Middle of str array: " + getMiddle(strArray));
// Test printPair
printPair("Name", "Alice");
printPair("Age", 25);
printPair("Salary", 50000.50);
// Test max method
System.out.println("Max of 10 and 20: " + max(10, 20));
System.out.println("Max of 'A' and 'Z': " + max('A', 'Z'));
System.out.println("Max of 3.14 and 2.71: " + max(3.14, 2.71));
// Test swap method
Integer[] numbers = {1, 2, 3, 4, 5};
System.out.println("Original: " + Arrays.toString(numbers));
swap(numbers, 0, 4);
System.out.println("After swap: " + Arrays.toString(numbers));
// Type inference in action
String[] fruits = {"Apple", "Banana", "Cherry"};
String[] swapped = swap(fruits, 0, 2); // Type inferred
System.out.println("Swapped fruits: " + Arrays.toString(swapped));
// Using generic method from utility class
System.out.println("Is empty string? " +
GenericUtils.isEmpty(""));
System.out.println("Is empty list? " +
GenericUtils.isEmpty(Arrays.asList()));
}
}
// Utility class with generic methods
class GenericUtils {
// Generic method to check if object is null
public static boolean isNull(T obj) {
return obj == null;
}
// Generic method for empty check
public static boolean isEmpty(T obj) {
if (obj == null) return true;
if (obj instanceof String) return ((String) obj).isEmpty();
if (obj instanceof java.util.Collection)
return ((java.util.Collection>) obj).isEmpty();
return false;
}
// Generic method to create array of given type
@SuppressWarnings("unchecked")
public static T[] createArray(Class type, int size) {
return (T[]) java.lang.reflect.Array.newInstance(type, size);
}
}
- Place type parameter before return type:
<T> returnType - Use descriptive type parameter names when possible
- Keep generic methods small and focused
- Document type constraints in method javadoc
- Consider using bounded type parameters for operations
- Test generic methods with different type arguments
4. Bounded Type Parameters
Bounded type parameters restrict the types that can be used as type arguments. You can specify that a type parameter must be a subtype of a particular class or implement certain interfaces.
import java.util.List;
import java.util.ArrayList;
// Upper bound - T must be Number or its subclass
class NumberProcessor {
private T number;
public NumberProcessor(T number) {
this.number = number;
}
public double getSquare() {
return number.doubleValue() * number.doubleValue();
}
public double getSquareRoot() {
return Math.sqrt(number.doubleValue());
}
public boolean isGreaterThan(T other) {
return number.doubleValue() > other.doubleValue();
}
}
// Multiple bounds - T must implement both Comparable and Serializable
class SortedContainer & java.io.Serializable> {
private List items = new ArrayList<>();
public void add(T item) {
items.add(item);
items.sort(null); // Uses natural ordering
}
public T getMax() {
if (items.isEmpty()) return null;
return items.get(items.size() - 1);
}
public T getMin() {
if (items.isEmpty()) return null;
return items.get(0);
}
}
// Interface bound
interface Drawable {
void draw();
}
class Circle implements Drawable {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing circle with radius: " + radius);
}
}
class Rectangle implements Drawable {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("Drawing rectangle: " + width + "x" + height);
}
}
class DrawingBoard {
private List shapes = new ArrayList<>();
public void addShape(T shape) {
shapes.add(shape);
}
public void drawAll() {
for (T shape : shapes) {
shape.draw();
}
}
}
// Generic method with bounded type parameter
class MathUtils {
public static double sum(List numbers) {
double total = 0.0;
for (T num : numbers) {
total += num.doubleValue();
}
return total;
}
public static > T findMax(T[] array) {
if (array == null || array.length == 0) {
return null;
}
T max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i].compareTo(max) > 0) {
max = array[i];
}
}
return max;
}
// Multiple bounds in method
public static > T clamp(T value, T min, T max) {
if (value.compareTo(min) < 0) return min;
if (value.compareTo(max) > 0) return max;
return value;
}
}
public class BoundedTypesExample {
public static void main(String[] args) {
// Test NumberProcessor with different Number types
NumberProcessor intProcessor = new NumberProcessor<>(5);
System.out.println("Integer square: " + intProcessor.getSquare());
System.out.println("Integer sqrt: " + intProcessor.getSquareRoot());
NumberProcessor doubleProcessor = new NumberProcessor<>(2.5);
System.out.println("Double square: " + doubleProcessor.getSquare());
// This won't compile - String is not a Number
// NumberProcessor stringProcessor = new NumberProcessor<>("test");
// Test SortedContainer
SortedContainer stringContainer = new SortedContainer<>();
stringContainer.add("Zebra");
stringContainer.add("Apple");
stringContainer.add("Banana");
System.out.println("Max string: " + stringContainer.getMax());
System.out.println("Min string: " + stringContainer.getMin());
// Test DrawingBoard
DrawingBoard board = new DrawingBoard<>();
board.addShape(new Circle(5.0));
board.addShape(new Rectangle(10.0, 20.0));
System.out.println("\nDrawing all shapes:");
board.drawAll();
// Test MathUtils generic methods
List integers = List.of(1, 2, 3, 4, 5);
System.out.println("Sum of integers: " + MathUtils.sum(integers));
List doubles = List.of(1.1, 2.2, 3.3);
System.out.println("Sum of doubles: " + MathUtils.sum(doubles));
// Test findMax
Integer[] intArray = {3, 1, 4, 1, 5};
System.out.println("Max integer: " + MathUtils.findMax(intArray));
String[] strArray = {"Apple", "Zebra", "Banana"};
System.out.println("Max string: " + MathUtils.findMax(strArray));
// Test clamp
Integer clamped = MathUtils.clamp(15, 10, 20);
System.out.println("Clamped 15 between 10-20: " + clamped);
Double clampedDouble = MathUtils.clamp(25.5, 10.0, 20.0);
System.out.println("Clamped 25.5 between 10.0-20.0: " + clampedDouble);
}
}
| Bound Type | Syntax | Description | Example |
|---|---|---|---|
| Upper Bound | <T extends Class> |
T must be subclass of Class | <T extends Number> |
| Interface Bound | <T extends Interface> |
T must implement Interface | <T extends Comparable> |
| Multiple Bounds | <T extends A & B> |
T must extend A and implement B | <T extends Number & Comparable> |
| Wildcard Bound | <? extends Class> |
Unknown type extending Class | List<? extends Number> |
5. Wildcards in Generics
Wildcards (?) represent unknown types in generics. They provide flexibility when you don't know or don't care about the specific type parameter.
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
public class UpperBoundedWildcard {
// Upper bounded wildcard - accepts List of any type that extends Number
public static double sumOfList(List extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
// Method that works with any List of Comparable objects
public static > T maxInRange(
List extends T> list, int begin, int end) {
if (list == null || list.isEmpty() || begin >= end) {
return null;
}
T max = list.get(begin);
for (int i = begin + 1; i < end && i < list.size(); i++) {
T current = list.get(i);
if (current.compareTo(max) > 0) {
max = current;
}
}
return max;
}
// Process list of numbers
public static void processNumbers(List extends Number> numbers) {
System.out.println("Processing numbers:");
for (Number num : numbers) {
System.out.println(" Value: " + num + ", Type: " +
num.getClass().getSimpleName());
}
}
public static void main(String[] args) {
// Test with different List types
List integers = Arrays.asList(1, 2, 3, 4, 5);
List doubles = Arrays.asList(1.1, 2.2, 3.3, 4.4);
List floats = Arrays.asList(1.5f, 2.5f, 3.5f);
System.out.println("Sum of integers: " + sumOfList(integers));
System.out.println("Sum of doubles: " + sumOfList(doubles));
System.out.println("Sum of floats: " + sumOfList(floats));
// Can't add to upper bounded wildcard list (except null)
List extends Number> numbers = integers;
// numbers.add(10); // Compile error!
numbers.add(null); // Only null is allowed
// Process different number lists
processNumbers(integers);
processNumbers(doubles);
// Test maxInRange
List scores = Arrays.asList(85, 92, 78, 95, 88);
Integer maxScore = maxInRange(scores, 0, 3);
System.out.println("Max score in range: " + maxScore);
List names = Arrays.asList("Alice", "Bob", "Charlie", "David");
String maxName = maxInRange(names, 1, 4);
System.out.println("Max name in range: " + maxName);
}
}
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
public class LowerBoundedWildcard {
// Lower bounded wildcard - accepts List of Integer or superclasses
public static void addIntegers(List super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i); // Can add Integer
}
}
// Copy from source to destination
public static void copy(List super T> dest, List extends T> src) {
for (T element : src) {
dest.add(element);
}
}
// Add numbers to a collection
public static void addNumbers(List super Number> list) {
list.add(10); // Integer
list.add(3.14); // Double
list.add(2.5f); // Float
list.add(100L); // Long
}
// Process consumer (can add items)
public static void processConsumer(List super String> list) {
list.add("Hello");
list.add("World");
// String item = list.get(0); // Can't safely get - returns Object
Object item = list.get(0); // Must use Object
}
public static void main(String[] args) {
// Test lower bounded wildcards
// Can add to List
List numbers = new ArrayList<>();
addIntegers(numbers);
System.out.println("Numbers after adding integers: " + numbers);
// Can add to List
- Producer (extends): When you only GET items from a structure
- Consumer (super): When you only PUT items into a structure
- Both (no wildcard): When you need to both get and put
- Mnemonic: "Get and Put Principle" or "PECS"
- Example:
copy(List<? super T> dest, List<? extends T> src)
6. Type Erasure and Bridge Methods
Type erasure is the process by which the Java compiler removes all type parameters and replaces them with their bounds or Object. This ensures backward compatibility with pre-generics code.
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
// Generic class that will undergo type erasure
public class ErasureDemo {
private T value;
public ErasureDemo(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
// Generic method
public E process(E input) {
System.out.println("Processing: " + input);
return input;
}
}
// Class demonstrating bridge methods
class ComparablePair> implements Comparable> {
private T first;
private T second;
public ComparablePair(T first, T second) {
this.first = first;
this.second = second;
}
@Override
public int compareTo(ComparablePair other) {
int firstCompare = this.first.compareTo(other.first);
if (firstCompare != 0) {
return firstCompare;
}
return this.second.compareTo(other.second);
}
// The compiler adds a bridge method:
// public int compareTo(Object other) {
// return compareTo((ComparablePair)other);
// }
}
public class TypeErasureExample {
public static void main(String[] args) throws Exception {
System.out.println("=== Type Erasure Demonstration ===");
// Create generic instances
ErasureDemo stringDemo = new ErasureDemo<>("Hello");
ErasureDemo intDemo = new ErasureDemo<>(42);
// Check runtime types - both are just ErasureDemo
System.out.println("stringDemo class: " + stringDemo.getClass());
System.out.println("intDemo class: " + intDemo.getClass());
System.out.println("Same class? " +
(stringDemo.getClass() == intDemo.getClass()));
// Reflection shows type erasure
System.out.println("\n=== Reflection on ErasureDemo ===");
Class> clazz = ErasureDemo.class;
System.out.println("Class name: " + clazz.getName());
// List all methods
System.out.println("\nMethods in ErasureDemo:");
for (Method method : clazz.getDeclaredMethods()) {
System.out.println(" " + method);
if (method.isSynthetic()) {
System.out.println(" ^ This is a synthetic/bridge method");
}
}
// Raw type usage (for backward compatibility)
System.out.println("\n=== Raw Types ===");
ErasureDemo rawDemo = new ErasureDemo("Test");
rawDemo.setValue("String"); // OK
rawDemo.setValue(123); // Also OK - no type checking!
// Unsafe cast warning
@SuppressWarnings("unchecked")
String value = (String) rawDemo.getValue(); // Potential ClassCastException
// Type erasure in arrays
System.out.println("\n=== Arrays and Generics ===");
// Generic array creation is not allowed
// ErasureDemo[] array = new ErasureDemo[10]; // Error
// But you can create raw array
ErasureDemo>[] safeArray = new ErasureDemo>[10]; // OK
safeArray[0] = new ErasureDemo<>("Test");
// Or use ArrayList instead
List> list = new ArrayList<>();
list.add(new ErasureDemo<>("Item1"));
// Bridge methods demonstration
System.out.println("\n=== Bridge Methods ===");
ComparablePair pair1 = new ComparablePair<>("A", "B");
ComparablePair pair2 = new ComparablePair<>("A", "C");
System.out.println("Compare result: " + pair1.compareTo(pair2));
// Check for bridge methods
Class> pairClass = ComparablePair.class;
System.out.println("\nMethods in ComparablePair:");
for (Method method : pairClass.getDeclaredMethods()) {
System.out.println(" " + method);
if (method.isBridge()) {
System.out.println(" ^ This is a BRIDGE method");
}
}
// Type erasure limitations
System.out.println("\n=== Type Erasure Limitations ===");
// Cannot use instanceof with generic types
Object obj = "Test";
// if (obj instanceof List) { } // Compile error
// Workaround
if (obj instanceof List) {
@SuppressWarnings("unchecked")
List list2 = (List) obj; // Unchecked cast
}
// Cannot create arrays of generic types
// T[] array = new T[10]; // Compile error
// Workaround using reflection
String[] strArray = createArray(String.class, 5);
System.out.println("Created array of type: " + strArray.getClass());
// Cannot overload methods with same erasure
// void process(List list) { }
// void process(List list) { } // Compile error - same erasure
}
// Helper method to create generic array using reflection
@SuppressWarnings("unchecked")
public static T[] createArray(Class type, int size) {
return (T[]) java.lang.reflect.Array.newInstance(type, size);
}
// Demonstration of overloading issue
public void processList(List list) {
System.out.println("Processing String list");
}
// This won't compile - same erasure as above
// public void processList(List list) {
// System.out.println("Processing Integer list");
// }
}
Type Erasure Limitations
- No
instanceofwith generic types - Cannot create arrays of generic types
- Cannot overload methods with same erasure
- Type information lost at runtime
- Cannot catch generic exceptions
- Cannot use primitives as type arguments
Workarounds
- Use
Class<T>parameter for type info - Use
ArrayListinstead of arrays - Use different method names for overloads
- Store type token for runtime type checking
- Use wrapper classes for primitives
- Consider using reflection when necessary
7. Practical Generics Examples
Example 1: Generic Repository Pattern
import java.util.*;
import java.util.function.Predicate;
// Generic Repository interface
interface Repository {
T save(T entity);
Optional findById(ID id);
List findAll();
List findBy(Predicate predicate);
void deleteById(ID id);
long count();
}
// In-memory implementation
class InMemoryRepository implements Repository {
private Map storage = new HashMap<>();
private IdGenerator idGenerator;
public InMemoryRepository(IdGenerator idGenerator) {
this.idGenerator = idGenerator;
}
@Override
public T save(T entity) {
ID id = idGenerator.generateId(entity);
storage.put(id, entity);
return entity;
}
@Override
public Optional findById(ID id) {
return Optional.ofNullable(storage.get(id));
}
@Override
public List findAll() {
return new ArrayList<>(storage.values());
}
@Override
public List findBy(Predicate predicate) {
return storage.values().stream()
.filter(predicate)
.toList();
}
@Override
public void deleteById(ID id) {
storage.remove(id);
}
@Override
public long count() {
return storage.size();
}
}
// ID generator interface
interface IdGenerator {
ID generateId(Object entity);
}
// Integer ID generator
class IntegerIdGenerator implements IdGenerator {
private int counter = 0;
@Override
public Integer generateId(Object entity) {
return ++counter;
}
}
// UUID generator
class UUIDGenerator implements IdGenerator {
@Override
public UUID generateId(Object entity) {
return UUID.randomUUID();
}
}
// Domain entities
class Product {
private String name;
private double price;
private String category;
public Product(String name, double price, String category) {
this.name = name;
this.price = price;
this.category = category;
}
// Getters and setters
public String getName() { return name; }
public double getPrice() { return price; }
public String getCategory() { return category; }
@Override
public String toString() {
return String.format("Product{name='%s', price=%.2f, category='%s'}",
name, price, category);
}
}
class Customer {
private String name;
private String email;
private int age;
public Customer(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
public String getName() { return name; }
public String getEmail() { return email; }
public int getAge() { return age; }
@Override
public String toString() {
return String.format("Customer{name='%s', email='%s', age=%d}",
name, email, age);
}
}
public class RepositoryPatternExample {
public static void main(String[] args) {
System.out.println("=== Generic Repository Pattern Example ===\n");
// Create repositories
Repository productRepo =
new InMemoryRepository<>(new IntegerIdGenerator());
Repository customerRepo =
new InMemoryRepository<>(new UUIDGenerator());
// Add products
Product laptop = productRepo.save(new Product("Laptop", 999.99, "Electronics"));
Product phone = productRepo.save(new Product("Phone", 699.99, "Electronics"));
Product book = productRepo.save(new Product("Java Book", 49.99, "Books"));
// Add customers
Customer alice = customerRepo.save(new Customer("Alice", "alice@email.com", 30));
Customer bob = customerRepo.save(new Customer("Bob", "bob@email.com", 25));
// Find operations
System.out.println("All Products:");
productRepo.findAll().forEach(System.out::println);
System.out.println("\nAll Customers:");
customerRepo.findAll().forEach(System.out::println);
// Find by predicate
System.out.println("\nExpensive Products (price > 500):");
productRepo.findBy(p -> p.getPrice() > 500)
.forEach(System.out::println);
System.out.println("\nElectronics Products:");
productRepo.findBy(p -> "Electronics".equals(p.getCategory()))
.forEach(System.out::println);
// Find by ID
System.out.println("\nFind product by ID 1:");
productRepo.findById(1).ifPresentOrElse(
System.out::println,
() -> System.out.println("Not found")
);
// Count
System.out.println("\nTotal products: " + productRepo.count());
System.out.println("Total customers: " + customerRepo.count());
// Delete
productRepo.deleteById(2);
System.out.println("\nAfter deleting product ID 2:");
System.out.println("Total products: " + productRepo.count());
// Generic utility method
System.out.println("\n=== Generic Utility Methods ===");
List products = productRepo.findAll();
List customers = customerRepo.findAll();
System.out.println("First product: " + getFirst(products));
System.out.println("First customer: " + getFirst(customers));
// Convert to map
Map productMap = toMap(
products,
p -> products.indexOf(p) + 1
);
System.out.println("\nProduct Map: " + productMap);
}
// Generic utility methods
public static T getFirst(List list) {
return list.isEmpty() ? null : list.get(0);
}
public static Map toMap(List list, Function keyExtractor) {
Map map = new HashMap<>();
for (T item : list) {
map.put(keyExtractor.apply(item), item);
}
return map;
}
}
// Functional interface for completeness
interface Function {
R apply(T t);
}
Example 2: Generic Math Calculator
import java.util.*;
import java.util.function.BinaryOperator;
// Generic calculator interface
interface Calculator {
T add(T a, T b);
T subtract(T a, T b);
T multiply(T a, T b);
T divide(T a, T b);
T power(T base, int exponent);
T squareRoot(T number);
}
// Generic calculator implementation
class GenericCalculator implements Calculator {
private final Class type;
private final BinaryOperator adder;
private final BinaryOperator multiplier;
public GenericCalculator(Class type,
BinaryOperator adder,
BinaryOperator multiplier) {
this.type = type;
this.adder = adder;
this.multiplier = multiplier;
}
@Override
public T add(T a, T b) {
return adder.apply(a, b);
}
@Override
public T subtract(T a, T b) {
// For simplicity, convert to double and back
double result = a.doubleValue() - b.doubleValue();
return convert(result);
}
@Override
public T multiply(T a, T b) {
return multiplier.apply(a, b);
}
@Override
public T divide(T a, T b) {
if (b.doubleValue() == 0) {
throw new ArithmeticException("Division by zero");
}
double result = a.doubleValue() / b.doubleValue();
return convert(result);
}
@Override
public T power(T base, int exponent) {
double result = Math.pow(base.doubleValue(), exponent);
return convert(result);
}
@Override
public T squareRoot(T number) {
if (number.doubleValue() < 0) {
throw new ArithmeticException("Square root of negative number");
}
double result = Math.sqrt(number.doubleValue());
return convert(result);
}
private T convert(double value) {
if (type == Integer.class) {
return type.cast((int) Math.round(value));
} else if (type == Double.class) {
return type.cast(value);
} else if (type == Float.class) {
return type.cast((float) value);
} else if (type == Long.class) {
return type.cast((long) Math.round(value));
}
throw new UnsupportedOperationException("Unsupported type: " + type);
}
}
// Statistics utility with generics
class Statistics> {
private List data;
public Statistics(List data) {
this.data = new ArrayList<>(data);
}
public T getMin() {
return data.stream().min(Comparable::compareTo).orElse(null);
}
public T getMax() {
return data.stream().max(Comparable::compareTo).orElse(null);
}
public double getMean() {
double sum = data.stream()
.mapToDouble(Number::doubleValue)
.sum();
return sum / data.size();
}
public double getMedian() {
List sorted = new ArrayList<>(data);
Collections.sort(sorted);
int size = sorted.size();
if (size % 2 == 0) {
double a = sorted.get(size/2 - 1).doubleValue();
double b = sorted.get(size/2).doubleValue();
return (a + b) / 2;
} else {
return sorted.get(size/2).doubleValue();
}
}
public double getStandardDeviation() {
double mean = getMean();
double variance = data.stream()
.mapToDouble(n -> Math.pow(n.doubleValue() - mean, 2))
.sum() / data.size();
return Math.sqrt(variance);
}
}
public class GenericMathExample {
public static void main(String[] args) {
System.out.println("=== Generic Math Calculator Example ===\n");
// Create calculators for different types
Calculator intCalculator = new GenericCalculator<>(
Integer.class,
Integer::sum,
(a, b) -> a * b
);
Calculator doubleCalculator = new GenericCalculator<>(
Double.class,
Double::sum,
(a, b) -> a * b
);
// Test integer calculator
System.out.println("Integer Calculator:");
System.out.println("10 + 5 = " + intCalculator.add(10, 5));
System.out.println("10 - 5 = " + intCalculator.subtract(10, 5));
System.out.println("10 * 5 = " + intCalculator.multiply(10, 5));
System.out.println("10 / 3 = " + intCalculator.divide(10, 3));
System.out.println("2 ^ 8 = " + intCalculator.power(2, 8));
System.out.println("√25 = " + intCalculator.squareRoot(25));
// Test double calculator
System.out.println("\nDouble Calculator:");
System.out.println("10.5 + 5.5 = " + doubleCalculator.add(10.5, 5.5));
System.out.println("10.5 - 5.5 = " + doubleCalculator.subtract(10.5, 5.5));
System.out.println("10.5 * 2.0 = " + doubleCalculator.multiply(10.5, 2.0));
System.out.println("10.0 / 3.0 = " + doubleCalculator.divide(10.0, 3.0));
System.out.println("2.5 ^ 3 = " + doubleCalculator.power(2.5, 3));
System.out.println("√30.25 = " + doubleCalculator.squareRoot(30.25));
// Statistics example
System.out.println("\n=== Statistics with Generics ===");
List intData = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Statistics intStats = new Statistics<>(intData);
System.out.println("Integer Statistics:");
System.out.println("Min: " + intStats.getMin());
System.out.println("Max: " + intStats.getMax());
System.out.println("Mean: " + intStats.getMean());
System.out.println("Median: " + intStats.getMedian());
System.out.println("Std Dev: " + intStats.getStandardDeviation());
List doubleData = Arrays.asList(1.5, 2.5, 3.5, 4.5, 5.5);
Statistics doubleStats = new Statistics<>(doubleData);
System.out.println("\nDouble Statistics:");
System.out.println("Min: " + doubleStats.getMin());
System.out.println("Max: " + doubleStats.getMax());
System.out.println("Mean: " + doubleStats.getMean());
System.out.println("Median: " + doubleStats.getMedian());
// Generic utility method for numeric operations
System.out.println("\n=== Generic Utility Methods ===");
List numbers = Arrays.asList(1, 2.5, 3, 4.75, 5);
System.out.println("Sum of numbers: " + sum(numbers));
System.out.println("Average of numbers: " + average(numbers));
// Generic method with bounded wildcard
System.out.println("\nMaximum in collections:");
System.out.println("Max int: " + findMax(intData));
System.out.println("Max double: " + findMax(doubleData));
}
// Generic method with upper bounded wildcard
public static double sum(List extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.sum();
}
public static double average(List extends Number> numbers) {
if (numbers.isEmpty()) return 0;
return sum(numbers) / numbers.size();
}
// Generic method to find maximum
public static > T findMax(List list) {
if (list.isEmpty()) return null;
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
}
8. Generics Best Practices
- Raw Types: Using generic types without type parameters
- Unchecked Casts: Using
@SuppressWarningswithout understanding - Overusing Wildcards: Making APIs unnecessarily complex
- Ignoring Type Erasure: Assuming type information available at runtime
- Complex Type Hierarchies: Creating hard-to-understand generic code
- Not Using Bounds: Missing opportunities for type safety
Generics Best Practices
- Always specify type parameters (avoid raw types)
- Use meaningful type parameter names (
T,E,K,V) - Apply PECS principle for maximum flexibility
- Document type constraints in javadoc
- Keep generic signatures simple and readable
- Test with different type arguments
- Consider performance implications of boxing/unboxing
Advanced Tips
- Use
Class<T>parameter when runtime type needed - Consider generic builders for complex object creation
- Use type tokens for reified generics patterns
- Leverage generic varargs carefully (
T...) - Understand covariance/contravariance with wildcards
- Use
Optionalfor nullable generic returns - Consider using libraries like Guava for advanced generics
When to Use Generics
Use Generics when: Creating collections, utility classes, algorithms working with multiple types
Avoid Generics when: Simple classes with single type, performance-critical code with primitives
Consider Alternatives: For runtime type checking, consider Class objects or reflection
Rule of Thumb: If you find yourself casting, generics might help