Bounded Type Parameters in Java
Learn how to restrict generic types to specific classes or interfaces
Think of bounded types like a parking lot that only accepts certain vehicles! You can create a parking lot for cars only, or for vehicles that have 4 wheels. Without bounds, your generic box can hold anything - a car, a boat, or a dinosaur! With bounds, you can say 'this box only holds vehicles' or 'this box only holds things with wheels'. Bounded types let you create more specific and safer generics!
The Problem Without Bounds
Without type bounds, you have no control over what types can be used. This can lead to type mismatches and runtime errors:
// Problem: No bounds means no type safety!public class Box<T> { private T value; public Box(T value) { this.value = value; } public T getValue() { return value; } // Problem: Can't call any methods on T! // What if we want to compare values? public boolean isGreaterThan(Box<T> other) { // WRONG! T doesn't have compareTo method! // return this.value.compareTo(other.value) > 0; // Compile error! return false; } // What if we want to work with numbers? public double toDouble() { // WRONG! T might not be a number! // return (Double) value; // Runtime error if T is String! return 0.0; }}public class NoBoundsProblems { public static void main(String[] args) { // These compile, but are problematic: Box<String> stringBox = new Box<>("Hello"); Box<Integer> intBox = new Box<>(42); // stringBox.isGreaterThan(intBox); // Can't compare String to Integer // intBox.toDouble(); // Might fail if we mix types // Without bounds, we can't rely on methods being available! // Solution: Use bounded types! 👇 }}What are Bounded Type Parameters?
Bounded type parameters restrict the types that can be used for a type parameter. You use the 'extends' keyword to set an upper bound, specifying that the type must be a subclass or implementer of a specific class or interface.
// Bounded Type Parameters let you restrict what types can be used!// Example 1: Upper bound with a single classpublic class NumberBox<T extends Number> { private T value; public NumberBox(T value) { this.value = value; } public T getValue() { return value; } // Now we know T is a Number, so we can call Number methods! public double asDouble() { return value.doubleValue(); // Number has doubleValue() } public int asInt() { return value.intValue(); // Number has intValue() }}// Example 2: Bound with interfacepublic class Comparable<T extends Comparable<T>> { private T value; public Comparable(T value) { this.value = value; } // Now we know T has compareTo method! public boolean isGreaterThan(T other) { return value.compareTo(other) > 0; // Safe! T is Comparable }}public class BoundedTypeIntro { public static void main(String[] args) { // NumberBox only accepts Number or its subclasses NumberBox<Integer> intBox = new NumberBox<>(42); System.out.println(intBox.asDouble()); // 42.0 NumberBox<Double> doubleBox = new NumberBox<>(3.14); System.out.println(doubleBox.asInt()); // 3 // NumberBox<String> stringBox = new NumberBox<>("Hello"); // Compile error! // Benefits of bounded types: // ✓ Type safety - only valid types are accepted // ✓ Can call specific methods on T // ✓ Compile-time error instead of runtime error // ✓ Self-documenting - shows what the generic expects }}Upper Bounds with extends
Use 'extends' to specify an upper bound - the type must be the specified class or a subclass of it:
// Upper Bounds: <T extends Class> means T must BE the class or a subclass// Example 1: Generic method with upper boundpublic class UpperBoundsExample { // T must be Number (or Integer, Double, Long, etc.) public static <T extends Number> void printAsDouble(T value) { System.out.println(value.doubleValue()); } // T must implement Comparable public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) > 0 ? a : b; } // T must extend Animal (or be Animal itself) public static <T extends Animal> void feedAnimal(T animal) { animal.eat(); // We know eat() exists! } // Multiple upper bounds: must extend all of them public static <T extends Number & Comparable<T>> void analyze(T value) { System.out.println("Value: " + value.doubleValue()); System.out.println("Class: " + value.getClass()); }}// Example 2: Generic class with upper boundpublic class Container<T extends Comparable<T>> { private T first; private T second; public Container(T first, T second) { this.first = first; this.second = second; } public T getMin() { return first.compareTo(second) < 0 ? first : second; } public T getMax() { return first.compareTo(second) > 0 ? first : second; } public boolean areEqual() { return first.compareTo(second) == 0; }}// Example classes for demonstrationclass Animal { public void eat() { System.out.println("Eating..."); }}class Dog extends Animal { @Override public void eat() { System.out.println("Dog is eating..."); }}class Cat extends Animal { @Override public void eat() { System.out.println("Cat is eating..."); }}public class UpperBoundsDemo { public static void main(String[] args) { // Works because Integer extends Number printAsDouble(42); // 42.0 printAsDouble(3.14); // 3.14 // Works because String implements Comparable String maxStr = max("apple", "zebra"); System.out.println("Max: " + maxStr); // zebra Integer maxInt = max(10, 20); System.out.println("Max: " + maxInt); // 20 // Works because Dog extends Animal Dog dog = new Dog(); feedAnimal(dog); // Dog is eating... // Works with Container Container<Integer> intContainer = new Container<>(5, 10); System.out.println("Min: " + intContainer.getMin()); // 5 System.out.println("Max: " + intContainer.getMax()); // 10 // Container<String> stringContainer = new Container<>("a", "b"); // This works too because String implements Comparable // printAsDouble("text"); // Compile error! String is not Number // feedAnimal(new Object()); // Compile error! Object is not Animal }}Lower Bounds with super
Use 'super' to specify a lower bound - the type must be the specified class or a superclass of it. This is commonly used with wildcards:
// Lower Bounds: <? super Class> means type must BE the class or a superclass// Used to safely ADD items to a collectionimport java.util.*;public class LowerBoundsExample { // Can read from list, but returns Object // Can write Integer and subclasses to the list public static void addIntegers(List<? super Integer> list) { list.add(10); // ✓ Can add Integer list.add(20); // ✓ Can add Integer // Object obj = list.get(0); // ✓ Can read, but returns Object // Integer num = list.get(0); // ✗ Compile error! Don't know if it's Integer } // Can write Number and subclasses public static void addNumbers(List<? super Number> list) { list.add(42); // ✓ Can add Integer (subclass of Number) list.add(3.14); // ✓ Can add Double (subclass of Number) list.add(100L); // ✓ Can add Long (subclass of Number) // Object obj = list.get(0); // ✓ Returns Object // Number num = list.get(0); // ✗ Compile error! Could be Object } // Copy from source to destination // We can read anything from source (List<? extends T>) // We can write T to destination (List<? super T>) public static <T> void copy(List<? extends T> source, List<? super T> destination) { for (T item : source) { destination.add(item); } } // Populate a collection with default values public static void fillWithDefaults(List<? super String> list) { list.add("default1"); list.add("default2"); list.add("default3"); }}// Example: Number hierarchy// Number (abstract class)// ├── Integer// ├── Double// ├── Long// └── Floatpublic class LowerBoundsDemo { public static void main(String[] args) { // List<Object> can accept <? super Integer> // because Object is superclass of Integer List<Object> objectList = new ArrayList<>(); addIntegers(objectList); System.out.println("Objects: " + objectList); // [10, 20] // List<Number> can accept <? super Integer> // because Number is superclass of Integer List<Number> numberList = new ArrayList<>(); addIntegers(numberList); System.out.println("Numbers: " + numberList); // [10, 20] // List<Integer> can accept <? super Integer> // because Integer is superclass of itself (or equal) List<Integer> intList = new ArrayList<>(); addIntegers(intList); System.out.println("Integers: " + intList); // [10, 20] // Copy example List<Integer> source = Arrays.asList(1, 2, 3); List<Object> destination = new ArrayList<>(); copy(source, destination); System.out.println("Copied: " + destination); // [1, 2, 3] // When to use lower bounds: // - Writing to a collection // - Method that adds items to a collection // - Generic method parameters that accept different types }}Multiple Bounds
A type parameter can have multiple bounds. The first bound must be a class (if used), and additional bounds must be interfaces:
// Multiple Bounds: T can have multiple bounds using &// Format: <T extends Class & Interface1 & Interface2>// First bound must be a class (if used), rest must be interfacesimport java.io.Serializable;// Define interfaces for demonstrationinterface Drawable { void draw();}interface Cloneable { Object clone() throws CloneNotSupportedException;}interface Resizable { void resize(int newSize);}// Example 1: Multiple boundspublic class Shape implements Drawable, Serializable, Comparable<Shape> { private String name; private int size; public Shape(String name, int size) { this.name = name; this.size = size; } @Override public void draw() { System.out.println("Drawing " + name); } @Override public int compareTo(Shape other) { return Integer.compare(this.size, other.size); } public void resize(int newSize) { this.size = newSize; } public String getName() { return name; } public int getSize() { return size; }}// Example 2: Multiple bounds in generic classpublic class ComparableDrawable<T extends Shape & Drawable & Comparable<T>> { private T item; public ComparableDrawable(T item) { this.item = item; } // We know T has all these methods! public void performAll() { item.draw(); // Has draw() from Drawable System.out.println("Size: " + item.getSize()); // Has getSize() from Shape } public boolean isLargerThan(ComparableDrawable<T> other) { return item.compareTo(other.item) > 0; // Has compareTo() from Comparable }}// Example 3: Multiple bounds in generic methodpublic class MultipleBoundsExample { // T must be: Shape, Drawable, and Comparable public static <T extends Shape & Drawable & Comparable<T>> void drawAndCompare( T shape1, T shape2) { shape1.draw(); // From Drawable shape2.draw(); // From Drawable if (shape1.compareTo(shape2) > 0) { // From Comparable System.out.println(shape1.getName() + " is larger"); } else { System.out.println(shape2.getName() + " is larger"); } } // Order matters: class first, then interfaces // <T extends ClassA & InterfaceB & InterfaceC> ✓ Correct // <T extends InterfaceA & ClassB & InterfaceC> ✗ Compile error! // Real-world example: Entity with tracking public static <T extends Object & Serializable & Comparable<T>> void processEntity(T entity) { System.out.println("Processing: " + entity); // Can call toString() // Can be serialized // Can be compared }}public class MultipleBoundsDemo { public static void main(String[] args) { Shape circle = new Shape("Circle", 50); Shape square = new Shape("Square", 40); // circle is Shape, Drawable, Serializable, Comparable ComparableDrawable<Shape> drawable = new ComparableDrawable<>(circle); drawable.performAll(); // Drawing Circle drawAndCompare(circle, square); // Circle is larger // Benefits of multiple bounds: // ✓ Enforce multiple constraints on one type parameter // ✓ More specific than single bound // ✓ Better type safety and code clarity // ✓ Can call methods from all bounds }}Bounded Wildcards in Methods
Wildcards can also have bounds, making your methods more flexible while maintaining type safety:
// Bounded Wildcards: Apply bounds to wildcards in method parameters// <? extends T> - upper bound: read from collection// <? super T> - lower bound: write to collectionimport java.util.*;public class BoundedWildcardsExample { // Upper bound: <? extends Number> // Accepts List<Integer>, List<Double>, etc. // Used when you WANT TO READ public static void printNumbers(List<? extends Number> numbers) { for (Number num : numbers) { System.out.println(num.doubleValue()); } // Can't add anything (except null) // numbers.add(42); // Compile error! } // Lower bound: <? super Integer> // Accepts List<Integer>, List<Number>, List<Object> // Used when you WANT TO WRITE public static void addIntegers(List<? super Integer> list) { list.add(10); list.add(20); list.add(30); // Can't get as Integer // Integer num = list.get(0); // Compile error! Returns Object } // Upper bound with Comparable public static <T extends Comparable<? super T>> T findMin(List<? extends T> list) { if (list.isEmpty()) return null; T min = list.get(0); for (T item : list) { if (item.compareTo(min) < 0) { min = item; } } return min; } // Copy method using both bounds // Source: read from it (upper bound) // Destination: write to it (lower bound) public static <T> void copy(List<? extends T> source, List<? super T> destination) { for (T item : source) { destination.add(item); } } // Process any Comparable with subtype wildcards public static <T extends Comparable<? super T>> void processComparable(List<? extends T> items) { for (T item : items) { System.out.println("Processing: " + item); } } // Unbounded wildcard (no bounds) public static void printAny(List<?> list) { for (Object obj : list) { System.out.println(obj); } }}public class BoundedWildcardsDemo { public static void main(String[] args) { // Upper bound: <? extends Number> List<Integer> integers = Arrays.asList(1, 2, 3); printNumbers(integers); // ✓ Works List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3); printNumbers(doubles); // ✓ Works // Lower bound: <? super Integer> List<Integer> intList = new ArrayList<>(); addIntegers(intList); // ✓ Works List<Number> numberList = new ArrayList<>(); addIntegers(numberList); // ✓ Works List<Object> objectList = new ArrayList<>(); addIntegers(objectList); // ✓ Works // Copy example List<String> source = Arrays.asList("apple", "banana", "cherry"); List<Object> destination = new ArrayList<>(); copy(source, destination); System.out.println("Copied: " + destination); // Find minimum Integer min = findMin(Arrays.asList(5, 2, 8, 1, 9)); System.out.println("Min: " + min); // 1 // Producer extends, Consumer super // List<? extends T> - Producer (reads from list) // List<? super T> - Consumer (writes to list) }}Real-World Examples
See how bounded types are used in practical applications:
import java.util.*;// Example 1: NumberBox with boundspublic class NumberBox<T extends Number> { private T value; public NumberBox(T value) { this.value = value; } public T getValue() { return value; } public double getAsDouble() { return value.doubleValue(); } public long getAsLong() { return value.longValue(); } public boolean isPositive() { return value.doubleValue() > 0; } public boolean isGreaterThan(T other) { return this.value.doubleValue() > other.doubleValue(); }}// Example 2: ComparableBoxpublic class ComparableBox<T extends Comparable<T>> { private T value; public ComparableBox(T value) { this.value = value; } public T getValue() { return value; } public boolean isLessThan(T other) { return value.compareTo(other) < 0; } public boolean equals(T other) { return value.compareTo(other) == 0; }}// Example 3: Sorting with boundspublic class Sorter { // Sort any list of comparable items public static <T extends Comparable<T>> void sort(List<T> list) { Collections.sort(list); } // Find maximum value public static <T extends Comparable<T>> T findMax(List<T> list) { if (list.isEmpty()) return null; T max = list.get(0); for (T item : list) { if (item.compareTo(max) > 0) { max = item; } } return max; } // Generic min/max with wildcards public static <T extends Comparable<? super T>> T findMin(List<? extends T> list) { if (list.isEmpty()) return null; T min = list.get(0); for (T item : list) { if (item.compareTo(min) < 0) { min = item; } } return min; }}// Example 4: Calculator with boundspublic class Calculator<T extends Number> { private T value; public Calculator(T value) { this.value = value; } public double add(T other) { return value.doubleValue() + other.doubleValue(); } public double subtract(T other) { return value.doubleValue() - other.doubleValue(); } public double multiply(T other) { return value.doubleValue() * other.doubleValue(); } public double divide(T other) { if (other.doubleValue() == 0) { throw new ArithmeticException("Division by zero"); } return value.doubleValue() / other.doubleValue(); }}// Example 5: Repository with Comparable entitiespublic interface Entity extends Comparable<Entity> { Integer getId();}public class SortableRepository<T extends Entity> { private List<T> items = new ArrayList<>(); public void add(T item) { items.add(item); } public List<T> getAll() { return new ArrayList<>(items); } public List<T> getAllSorted() { List<T> sorted = new ArrayList<>(items); Collections.sort(sorted); return sorted; } public T getMax() { if (items.isEmpty()) return null; return Collections.max(items); } public T getMin() { if (items.isEmpty()) return null; return Collections.min(items); }}public class RealWorldBoundedTypes { public static void main(String[] args) { // NumberBox examples NumberBox<Integer> intBox = new NumberBox<>(42); System.out.println("As double: " + intBox.getAsDouble()); // 42.0 System.out.println("Is positive: " + intBox.isPositive()); // true NumberBox<Double> doubleBox = new NumberBox<>(3.14); System.out.println("As long: " + doubleBox.getAsLong()); // 3 // ComparableBox examples ComparableBox<String> stringBox = new ComparableBox<>("apple"); System.out.println("Less than banana: " + stringBox.isLessThan("banana")); // true // Sorter examples List<Integer> integers = Arrays.asList(5, 2, 8, 1, 9); Sorter.sort(integers); System.out.println("Sorted: " + integers); // [1, 2, 5, 8, 9] Integer max = Sorter.findMax(Arrays.asList(10, 20, 5, 15)); System.out.println("Max: " + max); // 20 // Calculator examples Calculator<Integer> calc = new Calculator<>(10); System.out.println("10 + 5 = " + calc.add(5)); // 15.0 System.out.println("10 * 3 = " + calc.multiply(3)); // 30.0 System.out.println("10 / 2 = " + calc.divide(2)); // 5.0 // Real-world benefits: // ✓ Type safety - compile-time errors instead of runtime // ✓ Code reuse - one generic class for many types // ✓ Self-documenting - bounds show what types are supported // ✓ Can call specific methods - no casting needed }}Key Concepts
Upper Bound (extends)
Restricts the type to be a subclass of the specified class. Used with class or interface bounds.
Lower Bound (super)
Restricts the type to be a superclass of the specified class. Used mainly with wildcards.
Multiple Bounds
A type can have multiple bounds separated by &. First must be a class, rest must be interfaces.
Type Safety with Bounds
Bounds let you call specific methods on the type parameter, knowing they will exist.
Best Practices
- ✓Use bounds to make your generics more specific and type-safe
- ✓Use extends for upper bounds when you need to call specific methods
- ✓Use super for lower bounds when adding items to a collection
- ✓Understand that bounds only apply at compile time - they're erased at runtime
- ✓Document what bounds you're applying and why
- ✓Consider using wildcards with bounds for more flexible method signatures
- ✓Remember: no bounds means Object is the implicit upper bound
Common Mistakes
✗ Trying to use multiple class bounds
Why it's wrong: You can only extend one class, but you can implement multiple interfaces. First bound must be a class if used: <T extends A & B> is correct if A is a class and B is an interface!
✗ Confusing upper and lower bounds
Why it's wrong: extends = upper bound (IS A or subclass), super = lower bound (superclass). Use extends more often!
✗ Using bounds when an interface would work better
Why it's wrong: Sometimes extracting an interface is cleaner than using complex bounds.
✗ Forgetting that bounds don't apply at runtime
Why it's wrong: Type erasure removes bounds at runtime. You can't do 'instanceof T' or 'new T()' even with bounds!
Interview Tips
- 💡Explain the difference between upper bounds (extends) and lower bounds (super)
- 💡Know how to create a bounded generic class or method
- 💡Understand multiple bounds and the order they must be declared
- 💡Be able to explain when to use bounded types instead of plain generics
- 💡Know that bounds only exist at compile time - they're erased at runtime
- 💡Understand bounded wildcards and when to use <? extends T> vs <? super T>
- 💡Explain that upper bounds let you call methods on the bounded type