Bounded type parameters restrict the types that can be used as generic type arguments. They allow you to specify that a type parameter must be a subtype of certain classes or implement certain interfaces.
1. Upper Bounded Type Parameters
Basic Upper Bound Syntax
import java.util.*; public class UpperBoundBasics { // Upper bounded type parameter - T must be Number or its subclass public static <T extends Number> double sumOfList(List<T> list) { double sum = 0.0; for (T number : list) { sum += number.doubleValue(); // Can call Number methods } return sum; } // Multiple bounds - T must implement both Comparable and Serializable public static <T extends Comparable<T> & Serializable> 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; } // Upper bound with wildcards public static double sumOfNumbers(List<? extends Number> list) { double sum = 0.0; for (Number number : list) { sum += number.doubleValue(); } return sum; } public static void main(String[] args) { System.out.println("=== Upper Bounded Type Parameters ==="); // Test with Integer list (Integer extends Number) List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5); System.out.println("Sum of integers: " + sumOfList(integers)); // Test with Double list (Double extends Number) List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3); System.out.println("Sum of doubles: " + sumOfList(doubles)); // Test with wildcard method System.out.println("Sum using wildcard: " + sumOfNumbers(integers)); System.out.println("Sum using wildcard: " + sumOfNumbers(doubles)); // Multiple bounds example String[] names = {"Alice", "Bob", "Charlie"}; String maxName = findMax(names); System.out.println("Max name: " + maxName); demonstrateClassHierarchy(); } public static void demonstrateClassHierarchy() { System.out.println("\n=== Class Hierarchy with Bounds ==="); class Animal { String name; Animal(String name) { this.name = name; } void eat() { System.out.println(name + " is eating"); } } class Mammal extends Animal { Mammal(String name) { super(name); } void walk() { System.out.println(name + " is walking"); } } class Dog extends Mammal { Dog(String name) { super(name); } void bark() { System.out.println(name + " is barking"); } } class Cat extends Mammal { Cat(String name) { super(name); } void meow() { System.out.println(name + " is meowing"); } } // Generic method with upper bound public static <T extends Mammal> void performMammalActions(List<T> mammals) { for (T mammal : mammals) { mammal.eat(); // From Animal mammal.walk(); // From Mammal // mammal.bark(); // Compile error - not all mammals can bark } } List<Dog> dogs = Arrays.asList(new Dog("Buddy"), new Dog("Max")); List<Cat> cats = Arrays.asList(new Cat("Whiskers"), new Cat("Fluffy")); System.out.println("Dog actions:"); performMammalActions(dogs); System.out.println("\nCat actions:"); performMammalActions(cats); } } Upper Bounds with Custom Classes
import java.util.*; public class UpperBoundCustomClasses { // Custom hierarchy for demonstration static abstract class Shape implements Comparable<Shape> { protected String name; protected double area; public Shape(String name, double area) { this.name = name; this.area = area; } public abstract void draw(); @Override public int compareTo(Shape other) { return Double.compare(this.area, other.area); } @Override public String toString() { return String.format("%s (area: %.2f)", name, area); } } static class Circle extends Shape { private double radius; public Circle(String name, double radius) { super(name, Math.PI * radius * radius); this.radius = radius; } @Override public void draw() { System.out.println("Drawing circle: " + name + " with radius " + radius); } } static class Rectangle extends Shape { private double width, height; public Rectangle(String name, double width, double height) { super(name, width * height); this.width = width; this.height = height; } @Override public void draw() { System.out.println("Drawing rectangle: " + name + " " + width + "x" + height); } } static class Triangle extends Shape { private double base, height; public Triangle(String name, double base, double height) { super(name, 0.5 * base * height); this.base = base; this.height = height; } @Override public void draw() { System.out.println("Drawing triangle: " + name + " base=" + base + " height=" + height); } } // Generic methods with upper bounds public static <T extends Shape> void drawAllShapes(List<T> shapes) { for (T shape : shapes) { shape.draw(); } } public static <T extends Shape> T findLargestShape(List<T> shapes) { if (shapes.isEmpty()) return null; T largest = shapes.get(0); for (T shape : shapes) { if (shape.compareTo(largest) > 0) { largest = shape; } } return largest; } public static <T extends Shape & Comparable<Shape>> void sortShapes(List<T> shapes) { Collections.sort(shapes); } // Wildcard version public static void processShapes(List<? extends Shape> shapes) { for (Shape shape : shapes) { System.out.println("Processing: " + shape); } } public static void main(String[] args) { System.out.println("=== Upper Bounds with Custom Classes ==="); List<Circle> circles = Arrays.asList( new Circle("Small Circle", 5.0), new Circle("Large Circle", 15.0) ); List<Rectangle> rectangles = Arrays.asList( new Rectangle("Small Rect", 4.0, 3.0), new Rectangle("Large Rect", 10.0, 8.0) ); List<Triangle> triangles = Arrays.asList( new Triangle("Small Triangle", 3.0, 4.0), new Triangle("Large Triangle", 6.0, 8.0) ); // Draw all shapes System.out.println("Drawing circles:"); drawAllShapes(circles); System.out.println("\nDrawing rectangles:"); drawAllShapes(rectangles); // Find largest shapes System.out.println("\nLargest circle: " + findLargestShape(circles)); System.out.println("Largest rectangle: " + findLargestShape(rectangles)); // Process shapes using wildcard System.out.println("\nProcessing circles:"); processShapes(circles); System.out.println("\nProcessing triangles:"); processShapes(triangles); // Sort and display List<Shape> mixedShapes = new ArrayList<>(); mixedShapes.add(new Circle("Medium Circle", 10.0)); mixedShapes.add(new Rectangle("Medium Rect", 6.0, 8.0)); mixedShapes.add(new Triangle("Medium Triangle", 5.0, 12.0)); System.out.println("\nBefore sorting:"); mixedShapes.forEach(System.out::println); Collections.sort(mixedShapes); System.out.println("\nAfter sorting by area:"); mixedShapes.forEach(System.out::println); } } 2. Lower Bounded Type Parameters
Lower Bound Syntax and Usage
import java.util.*; public class LowerBoundExamples { // Lower bounded wildcard - can accept List<Integer>, List<Number>, List<Object> public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 5; i++) { list.add(i); // Can add Integer to any supertype of Integer } } // Lower bound in generic method public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (T element : src) { dest.add(element); } } // Multiple operations with lower bounds public static <T> void addAll(List<? super T> destination, T... elements) { for (T element : elements) { destination.add(element); } } public static void main(String[] args) { System.out.println("=== Lower Bounded Type Parameters ==="); // Test with different list types List<Integer> integerList = new ArrayList<>(); addNumbers(integerList); System.out.println("Integer list: " + integerList); List<Number> numberList = new ArrayList<>(); addNumbers(numberList); System.out.println("Number list: " + numberList); List<Object> objectList = new ArrayList<>(); addNumbers(objectList); System.out.println("Object list: " + objectList); // Copy operation demonstration demonstrateCopyOperation(); // PECS principle demonstration demonstratePECSPrinciple(); } public static void demonstrateCopyOperation() { System.out.println("\n=== Copy Operation with Bounds ==="); // Source list (producer - uses extends) List<Integer> source = Arrays.asList(1, 2, 3, 4, 5); // Destination lists (consumer - uses super) List<Integer> intDest = new ArrayList<>(); List<Number> numDest = new ArrayList<>(); List<Object> objDest = new ArrayList<>(); // Copy from source to different destinations copy(intDest, source); copy(numDest, source); copy(objDest, source); System.out.println("Source: " + source); System.out.println("Integer destination: " + intDest); System.out.println("Number destination: " + numDest); System.out.println("Object destination: " + objDest); } public static void demonstratePECSPrinciple() { System.out.println("\n=== PECS Principle (Producer Extends, Consumer Super) ==="); class Fruit { String name; Fruit(String name) { this.name = name; } @Override public String toString() { return name; } } class Apple extends Fruit { Apple(String name) { super(name); } } class Banana extends Fruit { Banana(String name) { super(name); } } // Producer - read only (uses extends) List<Apple> apples = Arrays.asList(new Apple("Red Apple"), new Apple("Green Apple")); List<? extends Fruit> fruitBasket = apples; // Can read as Fruit System.out.println("Reading from producer:"); for (Fruit fruit : fruitBasket) { System.out.println(" - " + fruit); } // Consumer - write only (uses super) List<Fruit> fruitBowl = new ArrayList<>(); List<? super Apple> appleSink = fruitBowl; // Can write Apples appleSink.add(new Apple("Fuji Apple")); appleSink.add(new Apple("Gala Apple")); // appleSink.add(new Fruit("Generic Fruit")); // Compile error! System.out.println("Consumer contains: " + fruitBowl); } } 3. Multiple Bounds
Multiple Bounds Syntax
import java.util.*; import java.io.Serializable; import java.time.LocalDateTime; public class MultipleBounds { // Interface definitions interface Identifiable { String getId(); void setId(String id); } interface Timestamped { LocalDateTime getCreatedAt(); void setCreatedAt(LocalDateTime time); } interface Auditable extends Serializable { String getCreatedBy(); void setCreatedBy(String user); } // Class implementing multiple interfaces static class Document implements Identifiable, Timestamped, Auditable { private String id; private String content; private LocalDateTime createdAt; private String createdBy; public Document(String id, String content, String createdBy) { this.id = id; this.content = content; this.createdAt = LocalDateTime.now(); this.createdBy = createdBy; } @Override public String getId() { return id; } @Override public void setId(String id) { this.id = id; } @Override public LocalDateTime getCreatedAt() { return createdAt; } @Override public void setCreatedAt(LocalDateTime time) { this.createdAt = time; } @Override public String getCreatedBy() { return createdBy; } @Override public void setCreatedBy(String user) { this.createdBy = user; } @Override public String toString() { return String.format("Document{id='%s', content='%s', createdBy='%s', at=%s}", id, content, createdBy, createdAt); } } static class Product implements Identifiable, Serializable { private String id; private String name; private double price; public Product(String id, String name, double price) { this.id = id; this.name = name; this.price = price; } @Override public String getId() { return id; } @Override public void setId(String id) { this.id = id; } @Override public String toString() { return String.format("Product{id='%s', name='%s', price=%.2f}", id, name, price); } } // Generic methods with multiple bounds public static <T extends Identifiable & Serializable> void saveToDatabase(T entity) { System.out.println("Saving to database: " + entity.getId()); // Serialization and database logic would go here } public static <T extends Identifiable & Timestamped & Auditable> void auditEntity(T entity) { System.out.printf("Audit: %s created by %s at %s%n", entity.getId(), entity.getCreatedBy(), entity.getCreatedAt()); } public static <T extends Comparable<T> & Serializable> 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; } // Multiple bounds with class and interfaces public static <T extends Number & Comparable<T> & Serializable> void processNumber(T number) { System.out.println("Processing number: " + number); System.out.println("Double value: " + number.doubleValue()); } public static void main(String[] args) { System.out.println("=== Multiple Bounds ==="); // Test with Document (implements all interfaces) Document doc = new Document("DOC-001", "Important content", "john_doe"); auditEntity(doc); saveToDatabase(doc); // Test with Product (implements Identifiable & Serializable) Product product = new Product("PROD-001", "Laptop", 999.99); saveToDatabase(product); // auditEntity(product); // Compile error - Product doesn't implement Timestamped & Auditable // Test with numbers Integer[] numbers = {5, 2, 8, 1, 9}; Integer maxNumber = findMax(numbers); System.out.println("Max number: " + maxNumber); processNumber(42); processNumber(3.14); demonstrateComplexBounds(); } public static void demonstrateComplexBounds() { System.out.println("\n=== Complex Multiple Bounds ==="); abstract class Entity implements Comparable<Entity>, Serializable { protected String id; public Entity(String id) { this.id = id; } public String getId() { return id; } @Override public int compareTo(Entity other) { return this.id.compareTo(other.id); } @Override public abstract String toString(); } class User extends Entity { private String username; public User(String id, String username) { super(id); this.username = username; } @Override public String toString() { return String.format("User{id='%s', username='%s'}", id, username); } } class Order extends Entity { private double amount; public Order(String id, double amount) { super(id); this.amount = amount; } @Override public String toString() { return String.format("Order{id='%s', amount=%.2f}", id, amount); } } // Method with complex bounds public static <T extends Entity & Comparable<Entity> & Serializable> void processEntity(T entity) { System.out.println("Processing entity: " + entity); System.out.println("Entity ID: " + entity.getId()); } User user = new User("USR-001", "alice"); Order order = new Order("ORD-001", 199.99); processEntity(user); processEntity(order); // Collection with bounded wildcard List<? extends Entity> entities = Arrays.asList(user, order); System.out.println("\nAll entities:"); for (Entity entity : entities) { System.out.println(" - " + entity); } } } 4. Bounded Type Parameters in Generic Classes
Generic Classes with Bounds
import java.util.*; import java.time.LocalDateTime; public class BoundedGenericClasses { // Generic class with upper bound static class NumberContainer<T extends Number> { private T number; public NumberContainer(T number) { this.number = number; } public T getNumber() { return number; } public void setNumber(T number) { this.number = number; } public double getDoubleValue() { return number.doubleValue(); } public int getIntValue() { return number.intValue(); } @Override public String toString() { return String.format("NumberContainer{value=%s, double=%.2f, int=%d}", number, getDoubleValue(), getIntValue()); } } // Generic class with multiple bounds static class AuditableContainer<T extends Comparable<T> & Serializable> { private T value; private LocalDateTime createdAt; private String createdBy; public AuditableContainer(T value, String createdBy) { this.value = value; this.createdAt = LocalDateTime.now(); this.createdBy = createdBy; } public T getValue() { return value; } public LocalDateTime getCreatedAt() { return createdAt; } public String getCreatedBy() { return createdBy; } public boolean isGreaterThan(T other) { return value.compareTo(other) > 0; } @Override public String toString() { return String.format("AuditableContainer{value=%s, createdBy='%s', at=%s}", value, createdBy, createdAt); } } // Generic class with lower bound (less common, usually used in methods) static class ConsumerContainer<T> { private List<? super T> items; public ConsumerContainer() { this.items = new ArrayList<>(); } public void addItem(T item) { items.add(item); } @SuppressWarnings("unchecked") public T getItem(int index) { // Need to cast since we only know it's supertype of T return (T) items.get(index); } public int size() { return items.size(); } @Override public String toString() { return "ConsumerContainer{items=" + items + "}"; } } public static void main(String[] args) { System.out.println("=== Bounded Type Parameters in Generic Classes ==="); // NumberContainer examples NumberContainer<Integer> intContainer = new NumberContainer<>(42); NumberContainer<Double> doubleContainer = new NumberContainer<>(3.14159); // NumberContainer<String> stringContainer = new NumberContainer<>("hello"); // Compile error! System.out.println("Integer container: " + intContainer); System.out.println("Double container: " + doubleContainer); // AuditableContainer examples AuditableContainer<String> stringContainer = new AuditableContainer<>("Important Data", "admin"); AuditableContainer<Integer> integerContainer = new AuditableContainer<>(100, "system"); System.out.println("\nString container: " + stringContainer); System.out.println("Integer container: " + integerContainer); System.out.println("Is 100 > 50? " + integerContainer.isGreaterThan(50)); // ConsumerContainer examples ConsumerContainer<Integer> consumer = new ConsumerContainer<>(); consumer.addItem(1); consumer.addItem(2); consumer.addItem(3); System.out.println("\nConsumer container: " + consumer); System.out.println("Item at index 1: " + consumer.getItem(1)); demonstrateAdvancedGenericClass(); } public static void demonstrateAdvancedGenericClass() { System.out.println("\n=== Advanced Generic Class with Bounds ==="); interface Validatable { boolean isValid(); } interface Persistable { String getStorageKey(); } class DataEntity implements Validatable, Persistable, Comparable<DataEntity> { private String id; private String data; public DataEntity(String id, String data) { this.id = id; this.data = data; } @Override public boolean isValid() { return id != null && !id.trim().isEmpty() && data != null && !data.trim().isEmpty(); } @Override public String getStorageKey() { return "entity_" + id; } @Override public int compareTo(DataEntity other) { return this.id.compareTo(other.id); } @Override public String toString() { return String.format("DataEntity{id='%s', data='%s', valid=%b}", id, data, isValid()); } } // Generic repository with multiple bounds class Repository<T extends Validatable & Persistable & Comparable<T>> { private Map<String, T> storage = new HashMap<>(); public void save(T entity) { if (entity.isValid()) { storage.put(entity.getStorageKey(), entity); System.out.println("Saved: " + entity); } else { System.out.println("Cannot save invalid entity: " + entity); } } public T findById(String key) { return storage.get(key); } public List<T> findAllSorted() { List<T> entities = new ArrayList<>(storage.values()); Collections.sort(entities); return entities; } public int size() { return storage.size(); } } Repository<DataEntity> repo = new Repository<>(); DataEntity validEntity = new DataEntity("001", "Valid data"); DataEntity invalidEntity = new DataEntity("", "Invalid data"); DataEntity anotherValid = new DataEntity("002", "More data"); repo.save(validEntity); repo.save(invalidEntity); // Won't be saved repo.save(anotherValid); System.out.println("\nAll sorted entities:"); repo.findAllSorted().forEach(System.out::println); System.out.println("Repository size: " + repo.size()); } } 5. Real-World Examples
Data Validation Framework
import java.util.*; import java.time.LocalDate; import java.time.Period; public class ValidationFramework { // Validation interfaces interface Validatable { boolean isValid(); List<String> getValidationErrors(); } interface Auditable { String getCreatedBy(); LocalDate getCreatedDate(); String getLastModifiedBy(); LocalDate getLastModifiedDate(); } interface BusinessEntity extends Validatable, Auditable { String getId(); String getName(); } // Concrete entity classes static class Customer implements BusinessEntity { private String id; private String name; private String email; private LocalDate birthDate; private String createdBy; private LocalDate createdDate; public Customer(String id, String name, String email, LocalDate birthDate, String createdBy) { this.id = id; this.name = name; this.email = email; this.birthDate = birthDate; this.createdBy = createdBy; this.createdDate = LocalDate.now(); } @Override public String getId() { return id; } @Override public String getName() { return name; } public String getEmail() { return email; } public LocalDate getBirthDate() { return birthDate; } @Override public String getCreatedBy() { return createdBy; } @Override public LocalDate getCreatedDate() { return createdDate; } @Override public String getLastModifiedBy() { return createdBy; } @Override public LocalDate getLastModifiedDate() { return createdDate; } @Override public boolean isValid() { return getValidationErrors().isEmpty(); } @Override public List<String> getValidationErrors() { List<String> errors = new ArrayList<>(); if (id == null || id.trim().isEmpty()) { errors.add("ID is required"); } if (name == null || name.trim().isEmpty()) { errors.add("Name is required"); } if (email == null || !email.contains("@")) { errors.add("Valid email is required"); } if (birthDate != null) { int age = Period.between(birthDate, LocalDate.now()).getYears(); if (age < 18) { errors.add("Customer must be at least 18 years old"); } } return errors; } @Override public String toString() { return String.format("Customer{id='%s', name='%s', email='%s', valid=%b}", id, name, email, isValid()); } } static class Product implements BusinessEntity { private String id; private String name; private double price; private int stock; private String createdBy; private LocalDate createdDate; public Product(String id, String name, double price, int stock, String createdBy) { this.id = id; this.name = name; this.price = price; this.stock = stock; this.createdBy = createdBy; this.createdDate = LocalDate.now(); } @Override public String getId() { return id; } @Override public String getName() { return name; } public double getPrice() { return price; } public int getStock() { return stock; } @Override public String getCreatedBy() { return createdBy; } @Override public LocalDate getCreatedDate() { return createdDate; } @Override public String getLastModifiedBy() { return createdBy; } @Override public LocalDate getLastModifiedDate() { return createdDate; } @Override public boolean isValid() { return getValidationErrors().isEmpty(); } @Override public List<String> getValidationErrors() { List<String> errors = new ArrayList<>(); if (id == null || id.trim().isEmpty()) { errors.add("Product ID is required"); } if (name == null || name.trim().isEmpty()) { errors.add("Product name is required"); } if (price < 0) { errors.add("Price cannot be negative"); } if (stock < 0) { errors.add("Stock cannot be negative"); } return errors; } @Override public String toString() { return String.format("Product{id='%s', name='%s', price=%.2f, stock=%d, valid=%b}", id, name, price, stock, isValid()); } } // Generic validation service with bounded type parameters static class ValidationService<T extends BusinessEntity> { public ValidationResult validate(T entity) { List<String> errors = entity.getValidationErrors(); boolean isValid = errors.isEmpty(); return new ValidationResult(entity, isValid, errors); } public List<ValidationResult> validateAll(List<T> entities) { List<ValidationResult> results = new ArrayList<>(); for (T entity : entities) { results.add(validate(entity)); } return results; } public <U extends T> void processValidEntities(List<U> entities, EntityProcessor<U> processor) { for (U entity : entities) { if (entity.isValid()) { processor.process(entity); } else { System.out.println("Skipping invalid entity: " + entity); } } } } // Support classes static class ValidationResult { private final BusinessEntity entity; private final boolean valid; private final List<String> errors; public ValidationResult(BusinessEntity entity, boolean valid, List<String> errors) { this.entity = entity; this.valid = valid; this.errors = errors; } public BusinessEntity getEntity() { return entity; } public boolean isValid() { return valid; } public List<String> getErrors() { return errors; } @Override public String toString() { return String.format("ValidationResult{entity=%s, valid=%b, errors=%s}", entity, valid, errors); } } interface EntityProcessor<T extends BusinessEntity> { void process(T entity); } public static void main(String[] args) { System.out.println("=== Data Validation Framework ==="); // Create sample data Customer validCustomer = new Customer("CUST001", "John Doe", "[email protected]", LocalDate.of(1990, 1, 1), "admin"); Customer invalidCustomer = new Customer("", "Jane Smith", "invalid-email", LocalDate.of(2010, 1, 1), "admin"); Product validProduct = new Product("PROD001", "Laptop", 999.99, 10, "admin"); Product invalidProduct = new Product("", "", -10, -5, "admin"); // Validate customers ValidationService<Customer> customerService = new ValidationService<>(); System.out.println("Customer validation:"); System.out.println(customerService.validate(validCustomer)); System.out.println(customerService.validate(invalidCustomer)); // Validate products ValidationService<Product> productService = new ValidationService<>(); System.out.println("\nProduct validation:"); System.out.println(productService.validate(validProduct)); System.out.println(productService.validate(invalidProduct)); // Process valid entities List<Customer> customers = Arrays.asList(validCustomer, invalidCustomer); System.out.println("\nProcessing valid customers:"); customerService.processValidEntities(customers, customer -> System.out.println("Processing: " + customer.getName())); // Batch validation List<Product> products = Arrays.asList(validProduct, invalidProduct); List<ValidationResult> productResults = productService.validateAll(products); System.out.println("\nBatch product validation results:"); productResults.forEach(System.out::println); } } Summary
Key Concepts:
- Upper Bounds (
extends):
- Restrict type parameter to be a subtype of specific class/interface
<T extends Number>- T must be Number or subclass- Allows access to methods of the bound type
- Lower Bounds (
super):
- Restrict type parameter to be a supertype of specific class
<? super Integer>- Can be Integer, Number, or Object- Used for write operations (PECS principle)
- Multiple Bounds:
<T extends ClassA & InterfaceB & InterfaceC>- Class must come first, then interfaces
- Type must satisfy all bounds
- Wildcard Bounds:
<? extends T>- Upper bounded wildcard (producer)<? super T>- Lower bounded wildcard (consumer)
Best Practices:
- Use upper bounds when you need to call specific methods
- Use PECS principle: Producer Extends, Consumer Super
- Prefer wildcards for more flexibility in method parameters
- Use multiple bounds for complex constraints
- Keep bounds minimal - only specify what's actually needed
Common Use Cases:
- Collections framework - sorting, copying with bounds
- Validation frameworks - entities with multiple interfaces
- Repository patterns - generic DAOs with constraints
- Utility methods - working with number types, comparable objects
- API design - enforcing contracts through bounds
Bounded type parameters provide type safety while maintaining flexibility in generic programming, making them essential for robust Java applications.