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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 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.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Bounded Type Parameters let you restrict what types can be used!
// Example 1: Upper bound with a single class
public 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 interface
public 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Upper Bounds: <T extends Class> means T must BE the class or a subclass
// Example 1: Generic method with upper bound
public 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 bound
public 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 demonstration
class 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// Lower Bounds: <? super Class> means type must BE the class or a superclass
// Used to safely ADD items to a collection
import 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
// └── Float
public 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// 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 interfaces
import java.io.Serializable;
// Define interfaces for demonstration
interface Drawable {
void draw();
}
interface Cloneable {
Object clone() throws CloneNotSupportedException;
}
interface Resizable {
void resize(int newSize);
}
// Example 1: Multiple bounds
public 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 class
public 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 method
public 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// Bounded Wildcards: Apply bounds to wildcards in method parameters
// <? extends T> - upper bound: read from collection
// <? super T> - lower bound: write to collection
import 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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import java.util.*;
// Example 1: NumberBox with bounds
public 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: ComparableBox
public 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 bounds
public 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 bounds
public 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 entities
public 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