Java Tutorial
🔍

Java Bounded Type Parameters

Java Bounded Type Parameters

A bounded type parameter restricts what types can fill a generic placeholder. <T> alone says "any reference type." <T extends Number> says "any reference type that is Number or a subtype of Number." That restriction does two things simultaneously: it limits the set of types the caller can use, and it expands what the method or class body can do with values of type T. Without the bound, the method body can only call Object methods — toString(), equals(), hashCode(). With <T extends Number>, it can additionally call intValue(), doubleValue(), and any other method declared on Number. The bound is the key that unlocks methods on T.

What Are Bounded Type Parameters?

A bounded type parameter places a constraint on the type argument that can substitute for T. The constraint is expressed with the extends keyword — even for interfaces — followed by the upper bound type.

SYNTAX — upper bound on a class or method type parameter:

  class Box<T extends Number> { ... }
  // T must be Number or a subtype: Integer, Double, Long, Float, etc.

  static <T extends Comparable<T>> T max(T a, T b) { ... }
  // T must implement Comparable<T>

  class Repository<T extends Entity & Serializable> { ... }
  // T must extend Entity AND implement Serializable
  // Only ONE class bound allowed, interfaces separated by &

WHERE BOUNDS APPEAR:

  On a class type parameter:
    class SortedBox<T extends Comparable<T>> { ... }

  On an interface type parameter:
    interface Summable<T extends Number> { T sum(List<T> items); }

  On a method type parameter:
    static <T extends Number> double total(List<T> items) { ... }

WHAT "extends" MEANS IN BOUNDS:
  'extends' in a bound covers BOTH class extension AND interface implementation.
  <T extends Runnable> works even though Runnable is an interface.
  There is no 'implements' keyword in bounds - 'extends' does both jobs.

Basic Overview - The Two Things Every Developer Needs to Know

1. BOUNDS RESTRICT WHAT GOES IN AND EXPAND WHAT YOU CAN DO

   Fresher view  : <T extends Number> means the caller can only use
                   Number, Integer, Double, Long, Float, and their
                   subtypes. Nothing else compiles.
                   Inside the method, you can call Number methods
                   (intValue, doubleValue, longValue) on T values
                   without any cast.

   Deeper view   : type erasure replaces T with its first bound in
                   bytecode. <T extends Number> compiles to a method
                   that accepts Number at the bytecode level. Without
                   a bound, T erases to Object. The bound determines
                   WHAT the erased type is, which determines which
                   methods are callable without a cast.

2. MULTIPLE BOUNDS - one class, then interfaces

   Fresher view  : <T extends Animal & Flyable & Swimmable> means
                   T must extend Animal AND implement both Flyable
                   and Swimmable. All three rules apply simultaneously.
                   Inside the class/method, you can call all of their
                   methods on T.

   Deeper view   : only ONE type in the bound can be a class (and it
                   must come first). All types after it must be interfaces.
                   This mirrors Java's single class inheritance rule.
                   At the bytecode level, T erases to the FIRST bound
                   (the class, if present, or the first interface).
                   Bridge methods handle the interface methods.

3. RECURSIVE BOUNDS - the self-referential pattern

   Fresher view  : <T extends Comparable<T>> looks strange but means
                   "T can compare itself to another T". String, Integer,
                   and LocalDate all satisfy this bound because they
                   implement Comparable with themselves as the type arg.

   Deeper view   : this is one of the most common and most misunderstood
                   patterns in Java generics. It is what makes a generic
                   sort method work: the sort needs to compare two T
                   values, which requires T to know how to compare
                   itself - hence Comparable<T> not just Comparable.
                   Without the <T> in Comparable<T>, the compareTo()
                   method would accept Object, and all type safety for
                   the comparison would be lost.

4. BOUNDS ON METHODS VS BOUNDS ON CLASSES

   Fresher view  : a method can have its own bound regardless of whether
                   the class is generic or bounded. The method's bound
                   is independent.

   Deeper view   : a method bound is checked at each call site for that
                   method specifically. A class bound is checked at every
                   point where the class is parameterized. You can have
                   both - a class <T extends Entity> with a method
                   <U extends Comparable<U>> inside it - T and U are
                   completely independent type variables.

Upper Bounds in Detail

Single Upper Bound

The most common form: one type after extends, which can be a class or interface. The type constraint and the method-call capability are two sides of the same coin.

1// File: SingleBoundDemo.java 2 3import java.util.List; 4 5public class SingleBoundDemo { 6 7 // WITHOUT a bound - T erases to Object - only Object methods callable on T 8 static <T> double sumBroken(List<T> items) { 9 double total = 0; 10 for (T item : items) { 11 // total += item.doubleValue(); // COMPILE ERROR - T erased to Object 12 // Cannot call doubleValue() because T could be anything at compile time 13 } 14 return total; // useless without the bound 15 } 16 17 // WITH <T extends Number> - T erases to Number - Number methods are callable 18 static <T extends Number> double sum(List<T> items) { 19 double total = 0; 20 for (T item : items) { 21 total += item.doubleValue(); // legal - Number declares doubleValue() 22 } 23 return total; 24 } 25 26 // Bound on a return-value utility - the bound lets the method work safely 27 static <T extends Comparable<T>> T clamp(T value, T min, T max) { 28 if (value.compareTo(min) < 0) return min; // compareTo() on T - needs the bound 29 if (value.compareTo(max) > 0) return max; 30 return value; 31 } 32 33 public static void main(String[] args) { 34 35 System.out.println("=== sum() with different Number subtypes ==="); 36 List<Integer> quantities = List.of(10, 25, 8, 40, 15); 37 System.out.println("Integer sum : " + sum(quantities)); 38 39 List<Double> prices = List.of(499.0, 1299.0, 799.0, 2499.0); 40 System.out.println("Double sum : " + sum(prices)); 41 42 List<Long> timestamps = List.of(1000L, 2000L, 3000L); 43 System.out.println("Long sum : " + sum(timestamps)); 44 45 System.out.println(); 46 47 System.out.println("=== clamp() with Integer and Double ==="); 48 System.out.println(clamp(150, 100, 200)); // 150 - within range 49 System.out.println(clamp(50, 100, 200)); // 100 - below min 50 System.out.println(clamp(300, 100, 200)); // 200 - above max 51 52 System.out.println(); 53 54 System.out.println("=== clamp() with String (Comparable<String>) ==="); 55 System.out.println(clamp("Mango", "Apple", "Watermelon")); // Mango - within range 56 System.out.println(clamp("Apricot", "Banana", "Papaya")); // Banana - below min 57 58 // The following would NOT COMPILE - Object does not satisfy Comparable<T>: 59 // System.out.println(sum(List.of(new Object()))); // COMPILE ERROR 60 // System.out.println(clamp(new Object(), ...)); // COMPILE ERROR 61 } 62}
Output:
=== sum() with different Number subtypes ===
Integer sum : 98.0
Double sum  : 5096.0
Long sum    : 6000.0

=== clamp() with Integer and Double ===
150
100
200

=== clamp() with String (Comparable<String>) ===
Mango
Banana

Multiple Bounds

When a type parameter needs to satisfy more than one contract, the & operator chains the bounds. The class bound (if any) must come first, followed by interface bounds.

1// File: MultipleBoundsDemo.java 2 3import java.io.Serializable; 4import java.util.ArrayList; 5import java.util.List; 6 7public class MultipleBoundsDemo { 8 9 // Base class - represents any domain entity 10 static abstract class Entity { 11 abstract String getId(); 12 abstract String getDisplayName(); 13 } 14 15 // Multiple bounds: T must extend Entity AND implement Comparable<T> 16 // Inside the method, both Entity methods AND compareTo() are available on T 17 static <T extends Entity & Comparable<T>> T findLargest(List<T> items) { 18 if (items == null || items.isEmpty()) { 19 throw new IllegalArgumentException("List must not be empty"); 20 } 21 T result = items.get(0); 22 for (T item : items) { 23 if (item.compareTo(result) > 0) { // Comparable<T> bound enables this 24 result = item; 25 } 26 } 27 System.out.println(" Winner entity: " + result.getDisplayName()); // Entity bound enables this 28 return result; 29 } 30 31 // Three bounds: abstract class + two interfaces 32 // T erases to Entity (the first bound) in bytecode 33 static <T extends Entity & Comparable<T> & Serializable> void processAndStore(T entity) { 34 System.out.println(" Entity ID : " + entity.getId()); // Entity method 35 System.out.println(" Display : " + entity.getDisplayName()); // Entity method 36 System.out.println(" Serializable: can be stored safely"); // Serializable bound 37 // Real code would serialize entity here using Serializable contract 38 } 39 40 // Product - extends Entity, implements Comparable<Product> and Serializable 41 static class Product extends Entity 42 implements Comparable<Product>, Serializable { 43 44 private final String productId; 45 private final String name; 46 private final double price; 47 48 Product(String productId, String name, double price) { 49 this.productId = productId; 50 this.name = name; 51 this.price = price; 52 } 53 54 @Override public String getId() { return productId; } 55 @Override public String getDisplayName() { return name + " (Rs." + price + ")"; } 56 57 @Override 58 public int compareTo(Product other) { 59 return Double.compare(this.price, other.price); 60 } 61 62 @Override 63 public String toString() { 64 return "Product[" + productId + ", " + name + ", Rs." + price + "]"; 65 } 66 } 67 68 public static void main(String[] args) { 69 70 List<Product> catalog = new ArrayList<>(List.of( 71 new Product("P001", "Wireless Mouse", 799.0), 72 new Product("P002", "Laptop Stand", 1299.0), 73 new Product("P003", "Mechanical Keyboard", 3499.0), 74 new Product("P004", "USB Hub", 999.0) 75 )); 76 77 System.out.println("=== findLargest with <T extends Entity & Comparable<T>> ==="); 78 Product mostExpensive = findLargest(catalog); 79 System.out.println("Result: " + mostExpensive); 80 81 System.out.println(); 82 83 System.out.println("=== processAndStore with <T extends Entity & Comparable<T> & Serializable> ==="); 84 processAndStore(catalog.get(0)); 85 86 // This would NOT COMPILE - a class that extends Entity but does NOT 87 // implement Comparable<T> cannot be passed to findLargest: 88 // class SimpleEntity extends Entity { ... } 89 // findLargest(List.of(new SimpleEntity())); // COMPILE ERROR 90 } 91}
Output:
=== findLargest with <T extends Entity & Comparable<T>> ===
  Winner entity: Mechanical Keyboard (Rs.3499.0)
Result: Product[P003, Mechanical Keyboard, Rs.3499.0]

=== processAndStore with <T extends Entity & Comparable<T> & Serializable> ===
  Entity ID   : P001
  Display     : Wireless Mouse (Rs.799.0)
  Serializable: can be stored safely

Recursive Type Bounds

A recursive bound is when the type parameter appears inside its own bound. <T extends Comparable<T>> is the canonical example. The pattern reads: "T is a type that knows how to compare itself to other values of the same type T."

WHY THE RECURSION IS NECESSARY:

  Naive attempt:
    <T extends Comparable> T max(T a, T b) { ... }

  Problem: raw Comparable means compareTo() accepts Object.
    String's compareTo() expects a String - but raw Comparable's
    compareTo() signature is compareTo(Object). You lose the type
    safety for the comparison itself.

  Correct:
    <T extends Comparable<T>> T max(T a, T b) { ... }

  Now: Comparable<T>'s compareTo() accepts exactly T.
    When T = String: compareTo(String) - correct.
    When T = Integer: compareTo(Integer) - correct.
    The comparison is type-safe end to end.

  The Self-Referential Pattern:
    T extends Comparable<T>
    means: "T implements Comparable<T>"
    which means: "T has a method int compareTo(T other)"
    which means: "T knows how to compare itself against another T"

  COMMON TYPES THAT SATISFY <T extends Comparable<T>>:
    String       implements Comparable<String>   -> yes
    Integer      implements Comparable<Integer>  -> yes
    Double       implements Comparable<Double>   -> yes
    LocalDate    implements Comparable<LocalDate> -> yes
    Object       does NOT implement Comparable   -> no
    List<String> does NOT implement Comparable   -> no
1// File: RecursiveBoundDemo.java 2 3import java.time.LocalDate; 4import java.util.List; 5 6public class RecursiveBoundDemo { 7 8 // <T extends Comparable<T>> - the self-referential bound that makes 9 // sorting and finding extremes work correctly for any sortable type 10 static <T extends Comparable<T>> T max(T first, T second) { 11 return first.compareTo(second) >= 0 ? first : second; 12 } 13 14 static <T extends Comparable<T>> T min(T first, T second) { 15 return first.compareTo(second) <= 0 ? first : second; 16 } 17 18 // Find the range (min and max) in a non-empty list 19 static <T extends Comparable<T>> String range(List<T> items) { 20 if (items.isEmpty()) throw new IllegalArgumentException("List must not be empty"); 21 T minimum = items.get(0); 22 T maximum = items.get(0); 23 for (T item : items) { 24 if (item.compareTo(minimum) < 0) minimum = item; 25 if (item.compareTo(maximum) > 0) maximum = item; 26 } 27 return minimum + " to " + maximum; 28 } 29 30 // Verify that a list is sorted in ascending order 31 static <T extends Comparable<T>> boolean isSorted(List<T> items) { 32 for (int i = 1; i < items.size(); i++) { 33 if (items.get(i).compareTo(items.get(i - 1)) < 0) return false; 34 } 35 return true; 36 } 37 38 public static void main(String[] args) { 39 40 System.out.println("=== max() and min() with different types ==="); 41 System.out.println("max(\"Ananya\", \"Priya\") = " + max("Ananya", "Priya")); 42 System.out.println("max(1499, 3499) = " + max(1499, 3499)); 43 System.out.println("max(LocalDate.of(2025,1,1), now) = " 44 + max(LocalDate.of(2025, 1, 1), LocalDate.of(2026, 6, 15))); 45 46 System.out.println(); 47 48 System.out.println("=== range() with order amounts ==="); 49 List<Double> amounts = List.of(499.0, 1299.0, 799.0, 3499.0, 199.0, 2099.0); 50 System.out.println("Order amounts range: Rs." + range(amounts)); 51 52 System.out.println(); 53 54 System.out.println("=== isSorted() ==="); 55 List<Integer> ascending = List.of(10, 20, 30, 40, 50); 56 List<Integer> random = List.of(30, 10, 50, 20, 40); 57 System.out.println("ascending is sorted: " + isSorted(ascending)); 58 System.out.println("random is sorted : " + isSorted(random)); 59 60 System.out.println(); 61 62 System.out.println("=== range() with dates ==="); 63 List<LocalDate> deliveryDates = List.of( 64 LocalDate.of(2026, 6, 10), 65 LocalDate.of(2026, 6, 5), 66 LocalDate.of(2026, 6, 18), 67 LocalDate.of(2026, 6, 1) 68 ); 69 System.out.println("Delivery date range: " + range(deliveryDates)); 70 } 71}
Output:
=== max() and min() with different types ===
max("Ananya", "Priya")          = Priya
max(1499, 3499)                   = 3499
max(LocalDate.of(2025,1,1), now)  = 2026-06-15

=== range() with order amounts ===
Order amounts range: Rs.199.0 to Rs.3499.0

=== isSorted() ===
ascending is sorted: true
random is sorted   : false

=== range() with dates ===
Delivery date range: 2026-06-01 to 2026-06-18

Bounded Type Parameters on Generic Classes

Bounds are not exclusive to methods — a class can declare a bounded type parameter, restricting every instantiation of that class.

1// File: BoundedClassDemo.java 2 3import java.util.ArrayList; 4import java.util.Collections; 5import java.util.List; 6 7public class BoundedClassDemo { 8 9 // SortedContainer<T> only accepts types that can be compared. 10 // Every SortedContainer is always kept in sorted order. 11 // The bound <T extends Comparable<T>> ensures: 12 // 1. Only sortable types can parameterize this class 13 // 2. compareTo() is callable inside the class body 14 static class SortedContainer<T extends Comparable<T>> { 15 private final List<T> items = new ArrayList<>(); 16 17 void add(T item) { 18 items.add(item); 19 Collections.sort(items); // requires Comparable - guaranteed by the bound 20 } 21 22 T smallest() { 23 if (items.isEmpty()) throw new java.util.NoSuchElementException("Container is empty"); 24 return items.get(0); // always sorted ascending 25 } 26 27 T largest() { 28 if (items.isEmpty()) throw new java.util.NoSuchElementException("Container is empty"); 29 return items.get(items.size() - 1); 30 } 31 32 List<T> snapshot() { 33 return List.copyOf(items); 34 } 35 36 int size() { return items.size(); } 37 } 38 39 public static void main(String[] args) { 40 41 System.out.println("=== SortedContainer<Integer> - stock levels ==="); 42 SortedContainer<Integer> stockLevels = new SortedContainer<>(); 43 stockLevels.add(45); 44 stockLevels.add(12); 45 stockLevels.add(78); 46 stockLevels.add(3); 47 stockLevels.add(56); 48 System.out.println("Sorted: " + stockLevels.snapshot()); 49 System.out.println("Min : " + stockLevels.smallest()); 50 System.out.println("Max : " + stockLevels.largest()); 51 52 System.out.println(); 53 54 System.out.println("=== SortedContainer<String> - city names ==="); 55 SortedContainer<String> cities = new SortedContainer<>(); 56 cities.add("Hyderabad"); 57 cities.add("Bengaluru"); 58 cities.add("Mumbai"); 59 cities.add("Chennai"); 60 System.out.println("Sorted: " + cities.snapshot()); 61 System.out.println("First : " + cities.smallest()); 62 System.out.println("Last : " + cities.largest()); 63 64 // This would NOT COMPILE - no bound satisfied: 65 // SortedContainer<Object> invalid = new SortedContainer<>(); 66 // Object does not implement Comparable<Object> 67 } 68}
Output:
=== SortedContainer<Integer> - stock levels ===
Sorted: [3, 12, 45, 56, 78]
Min   : 3
Max   : 78

=== SortedContainer<String> - city names ===
Sorted: [Bengaluru, Chennai, Hyderabad, Mumbai]
First : Bengaluru
Last  : Mumbai

How Bounds Affect Type Erasure

This is what distinguishes developers who understand generics from those who merely use them. Every type parameter gets erased to its first bound at bytecode level.

ERASURE RULES FOR BOUNDED TYPE PARAMETERS:

  Declaration                    Erased to
  ─────────────────────────────────────────────────────────────
  <T>                            Object
  <T extends Number>             Number
  <T extends Comparable<T>>      Comparable
  <T extends Entity>             Entity
  <T extends Entity & Serializable>  Entity   (first bound)

  WHY THE FIRST BOUND MATTERS:

  class Box<T extends Number & Comparable<T>> {
      T value;
      void doSomething() {
          double d = value.doubleValue(); // Number method - available
          int cmp = value.compareTo(??);  // Comparable method - available via bridge
      }
  }

  Bytecode representation:
    Number value; // T erased to Number (first bound)
    void doSomething() {
        double d = value.doubleValue();         // direct Number call
        int cmp = ((Comparable) value).compareTo(??); // bridge cast to second bound
    }

  THIS EXPLAINS TWO THINGS:
  1. Why you can call Number methods on T without a cast (T IS Number in bytecode)
  2. Why interface bounds after the first require bridge methods
     (they need a CHECKCAST at the bytecode level)

  THE ORDER OF BOUNDS MATTERS FOR ERASURE:
  <T extends Number & Comparable<T>>  vs  <T extends Comparable<T> & Number>
  Both compile, but the first erases T to Number, the second to Comparable.
  For most code this makes no observable difference, but it affects
  which methods are accessible without a bridge cast in bytecode.
  Convention: put the class bound first.

Real-World Example - Groww Investment Analytics

A financial analytics platform processes multiple types of investment data — fund NAVs, stock prices, SIP amounts, and portfolio values — across different numeric types. Generic bounded methods that work with any Number subtype process all of them without duplication, while bounded comparisons handle ranking and sorting across all investment categories using the self-referential Comparable<T> bound.

1// File: InvestmentRecord.java 2 3public record InvestmentRecord( 4 String instrumentId, 5 String instrumentName, 6 String category, 7 double currentValue, 8 double previousValue 9) { 10 double changePercent() { 11 return previousValue == 0 ? 0 : 12 ((currentValue - previousValue) / previousValue) * 100.0; 13 } 14}
1// File: InvestmentAnalytics.java 2 3import java.util.*; 4import java.util.function.*; 5import java.util.stream.*; 6 7public class InvestmentAnalytics { 8 9 // Computes average of any List<T> where T is a Number subtype. 10 // Works for List<Double>, List<Float>, List<Integer> without separate methods. 11 public static <T extends Number> double average(List<T> values) { 12 if (values == null || values.isEmpty()) return 0.0; 13 double sum = 0; 14 for (T value : values) { 15 sum += value.doubleValue(); // Number.doubleValue() - requires the bound 16 } 17 return sum / values.size(); 18 } 19 20 // Returns the median of a list of any Comparable Number subtype. 21 // Sorts the list internally - requires both Number and Comparable<T>. 22 public static <T extends Number & Comparable<T>> double median(List<T> values) { 23 if (values == null || values.isEmpty()) return 0.0; 24 List<T> sorted = new ArrayList<>(values); 25 Collections.sort(sorted); // Comparable<T> enables this sort 26 int size = sorted.size(); 27 if (size % 2 == 0) { 28 return (sorted.get(size / 2 - 1).doubleValue() 29 + sorted.get(size / 2).doubleValue()) / 2.0; 30 } 31 return sorted.get(size / 2).doubleValue(); 32 } 33 34 // Returns top N instruments ranked by a numeric score extracted 35 // from each record. The scorer function returns a Comparable value 36 // so items can be ranked regardless of what metric is used. 37 public static <T extends InvestmentRecord, S extends Comparable<S>> 38 List<T> topN(List<T> records, Function<T, S> scorer, int n) { 39 return records.stream() 40 .sorted(Comparator.comparing(scorer).reversed()) 41 .limit(n) 42 .collect(Collectors.toList()); 43 } 44 45 // Counts how many values fall within a given range. 46 // <T extends Number & Comparable<T>> needed for doubleValue AND compareTo 47 public static <T extends Number & Comparable<T>> int countInRange( 48 List<T> values, T low, T high) { 49 int count = 0; 50 for (T value : values) { 51 if (value.compareTo(low) >= 0 && value.compareTo(high) <= 0) { 52 count++; 53 } 54 } 55 return count; 56 } 57}
1// File: GrowwAnalyticsDemo.java 2 3import java.util.List; 4 5public class GrowwAnalyticsDemo { 6 7 public static void main(String[] args) { 8 9 List<InvestmentRecord> portfolio = List.of( 10 new InvestmentRecord("NIFTY50", "Nifty 50 Index Fund", "Large Cap", 52000.0, 48000.0), 11 new InvestmentRecord("MIDCAP", "Midcap 150 Fund", "Mid Cap", 38000.0, 42000.0), 12 new InvestmentRecord("SMALLCAP", "Smallcap 250 Fund", "Small Cap", 21000.0, 18000.0), 13 new InvestmentRecord("GOLD_ETF", "Gold ETF", "Commodity", 67000.0, 62000.0), 14 new InvestmentRecord("FLEXICAP", "Flexi Cap Fund", "Flexi Cap", 44500.0, 41000.0) 15 ); 16 17 System.out.println("=== average() with <T extends Number> ==="); 18 List<Double> navValues = List.of(52000.0, 38000.0, 21000.0, 67000.0, 44500.0); 19 List<Integer> unitCounts = List.of(120, 85, 200, 45, 160); 20 System.out.printf("Average NAV : Rs.%.2f%n", InvestmentAnalytics.average(navValues)); 21 System.out.printf("Average unit count: %.1f%n", InvestmentAnalytics.average(unitCounts)); 22 23 System.out.println(); 24 25 System.out.println("=== median() with <T extends Number & Comparable<T>> ==="); 26 List<Double> changePercents = List.of(8.33, -9.52, 16.67, 8.06, 8.54); 27 System.out.printf("Median change %%: %.2f%%%n", InvestmentAnalytics.median(changePercents)); 28 29 System.out.println(); 30 31 System.out.println("=== topN() by current value ==="); 32 List<InvestmentRecord> topByValue = 33 InvestmentAnalytics.topN(portfolio, InvestmentRecord::currentValue, 3); 34 System.out.println("Top 3 by current value:"); 35 topByValue.forEach(r -> System.out.printf(" %-25s Rs.%,.0f%n", 36 r.instrumentName(), r.currentValue())); 37 38 System.out.println(); 39 40 System.out.println("=== topN() by change percent ==="); 41 List<InvestmentRecord> topGainers = 42 InvestmentAnalytics.topN(portfolio, InvestmentRecord::changePercent, 3); 43 System.out.println("Top 3 gainers:"); 44 topGainers.forEach(r -> System.out.printf(" %-25s %+.2f%%%n", 45 r.instrumentName(), r.changePercent())); 46 47 System.out.println(); 48 49 System.out.println("=== countInRange() - NAVs between Rs.30k and Rs.55k ==="); 50 int inRange = InvestmentAnalytics.countInRange(navValues, 30000.0, 55000.0); 51 System.out.println("NAV values in range Rs.30,000 - Rs.55,000: " + inRange + " out of " + navValues.size()); 52 } 53}
Output:
=== average() with <T extends Number> ===
Average NAV       : Rs.44500.00
Average unit count: 122.0

=== median() with <T extends Number & Comparable<T>> ===
Median change %: 8.33%

=== topN() by current value ===
Top 3 by current value:
  Gold ETF                  Rs.67,000
  Nifty 50 Index Fund       Rs.52,000
  Flexi Cap Fund            Rs.44,500

=== topN() by change percent ===
Top 3 gainers:
  Smallcap 250 Fund         +16.67%
  Nifty 50 Index Fund       +8.54%
  Flexi Cap Fund            +8.06%

=== countInRange() - NAVs between Rs.30k and Rs.55k ===
NAV values in range Rs.30,000 - Rs.55,000: 3 out of 5

average() works with List<Double> and List<Integer> from the same declaration because <T extends Number> makes doubleValue() available on any T. median() needs both Number (for doubleValue()) and Comparable<T> (for Collections.sort()), which is why both bounds appear. topN() uses a scorer function that returns any Comparable<S> — the type of the score and the type of the records are independent type parameters, each with their own bounds.

Bounded Type Parameters - Quick Reference

Bound SyntaxWhat T Must BeMethods Callable on T (beyond Object)
<T>Any reference typeNone (only Object methods)
<T extends Number>Number or any subtypeintValue(), doubleValue(), longValue(), etc.
<T extends Comparable<T>>Any self-comparable typecompareTo(T)
<T extends Number & Comparable<T>>A Number that is also self-comparableAll of the above
<T extends Entity>Entity or any subclassAll public methods of Entity
<T extends Entity & Serializable>A subclass of Entity that is SerializableAll Entity methods

Best Practices

Use the most permissive bound that satisfies the method's needs. If the method only needs compareTo(), bound to Comparable<T> alone — not Number & Comparable<T>. Wider bounds restrict more callers unnecessarily. The bound should be the minimum contract the method requires to do its job.

Always put the class bound first when combining multiple bounds. <T extends Number & Comparable<T>> is correct; <T extends Comparable<T> & Number> also compiles but places Comparable as the erased type instead of Number, which matters for which method calls require bridge casts in bytecode. Convention and correctness both favor the class first.

Use <T extends Comparable<T>> instead of the raw <T extends Comparable> for sorting and comparison. The raw form loses the type safety of the comparison itself — compareTo() would accept Object instead of T. Every place where two values of type T need to be ordered, the recursive bound is the correct form.

Prefer bounded type parameters over instanceof checks inside a generic method. A method that bounds its type parameter and calls the bound's methods is checked at compile time. A method that accepts Object, checks instanceof Number at runtime, and casts is not — it is exactly the pre-generics pattern that bounded type parameters were designed to replace.

Common Mistakes

Mistake 1 - Using extends For Both Class and Interface Bounds Interchangeably

1// BOTH of these are correct syntax - 'extends' works for classes AND interfaces 2// in a bound. There is no 'implements' keyword in type parameter bounds. 3static <T extends Number> double sum(List<T> items) { ... } // class bound 4static <T extends Runnable> void runAll(List<T> tasks) { ... } // interface bound 5 6// WRONG - 'implements' is not valid in a type parameter bound 7// COMPILE ERROR: "expected: identifier" 8// static <T implements Runnable> void runAll(List<T> tasks) { ... } 9 10// CORRECT - always use 'extends' regardless of whether the bound is a class or interface 11static <T extends Comparable<T> & java.io.Serializable> void process(T item) { ... }

Mistake 2 - Declaring the Class Bound After an Interface Bound

1import java.io.Serializable; 2 3// WRONG - if a class bound is present, it MUST come first. 4// Putting the class (Number) after the interface (Comparable) is a COMPILE ERROR. 5// "a class type may not follow interface types in a type bound" 6static <T extends Comparable<T> & Number> double sumBound(java.util.List<T> items) { 7 return 0; // COMPILE ERROR - Number must come before Comparable<T> 8} 9 10// CORRECT - class bound first, then interfaces 11static <T extends Number & Comparable<T>> double sumFixed(java.util.List<T> items) { 12 double total = 0; 13 for (T item : items) { 14 total += item.doubleValue(); 15 } 16 return total; 17}

Mistake 3 - Using Raw Comparable Instead of Comparable With a Type Argument

1import java.util.List; 2 3// WRONG - raw Comparable bound. compareTo() accepts Object, not T. 4// The comparison loses type safety - a String could be compared to 5// an Integer through this method if called carelessly with raw types. 6@SuppressWarnings("rawtypes") 7static <T extends Comparable> T maxRaw(T first, T second) { 8 return first.compareTo(second) >= 0 ? first : second; 9 // compareTo accepts Object here - dangerous with mixed types 10} 11 12// CORRECT - Comparable<T> ensures compareTo accepts exactly T 13static <T extends Comparable<T>> T maxTyped(T first, T second) { 14 return first.compareTo(second) >= 0 ? first : second; 15 // compareTo accepts only T - type-safe end to end 16}

Mistake 4 - Calling Methods Not Declared in the Bound

1import java.util.List; 2 3// WRONG - T is bounded to Number, so Number methods are available. 4// But calling a method specific to Integer (not declared on Number) 5// requires a cast - and that cast is unchecked and unsafe. 6static <T extends Number> void printBitCount(List<T> items) { 7 for (T item : items) { 8 // item.bitCount(); // COMPILE ERROR - Number has no bitCount() 9 // Calling an Integer-specific method requires a cast: 10 // ((Integer) item).bitCount(); // unchecked at runtime - ClassCastException if T != Integer 11 } 12} 13 14// CORRECT OPTION A - narrow the bound to Integer specifically (no longer generic) 15static void printBitCountIntegers(List<Integer> items) { 16 for (Integer item : items) { 17 System.out.println(Integer.bitCount(item)); // safe - Integer is known 18 } 19} 20 21// CORRECT OPTION B - if it must work for any Number, check and handle explicitly 22static <T extends Number> void printBitCountSafe(List<T> items) { 23 for (T item : items) { 24 if (item instanceof Integer integer) { 25 System.out.println("Bit count: " + Integer.bitCount(integer)); 26 } else { 27 System.out.println("Bit count not available for: " + item.getClass().getSimpleName()); 28 } 29 } 30}

Interview Questions

Q1. What is a bounded type parameter in Java, and what does it do?

A bounded type parameter restricts the set of types that can substitute for a generic type variable, expressed as <T extends SomeType>. It simultaneously restricts callers (only types satisfying the bound can be used) and expands the method or class body's capabilities (methods declared on SomeType become callable on T values without a cast). Without a bound, T erases to Object and only Object methods are available. With <T extends Number>, T erases to Number and all of Number's methods become available on any T value inside the class or method.

Q2. Why is the keyword extends used for both class and interface bounds in type parameters?

Java chose extends as the single keyword for bounds regardless of whether the bound is a class or an interface, to keep the generics syntax consistent. A normal class declaration uses extends for class inheritance and implements for interface implementation — but a type parameter bound uses only extends for both. Writing <T implements Runnable> is a compile error; the correct form is <T extends Runnable> even though Runnable is an interface. This is purely a syntax decision by the Java language designers; the semantic meaning is equivalent to "T must be compatible with this type."

Q3. What is a recursive type bound, and why is <T extends Comparable> written the way it is?

A recursive type bound has the type parameter appearing inside its own bound. <T extends Comparable<T>> means "T is a type that implements Comparable<T> — T knows how to compare itself against another T." The recursion is necessary to preserve type safety in the comparison. Without it, <T extends Comparable> would mean compareTo() accepts Object instead of T, allowing a String to be incorrectly compared against an Integer through the method. With Comparable<T>, the compareTo() method accepts exactly T, so the compiler verifies the types on both sides of every comparison made inside the generic method.

Q4. What are the rules for combining multiple bounds with the & operator?

Multiple bounds are joined with &. At most one type in the bound can be a class (not an interface), and if a class appears in the bound, it must come first — <T extends Number & Comparable<T>> is correct, but <T extends Comparable<T> & Number> is a compile error. Any number of interface bounds can follow the class bound. At the bytecode level, T erases to the first bound — the class if present, or the first interface otherwise — and the remaining bounds are handled via bridge methods and casts. Each bound adds both a restriction on callers and additional method accessibility inside the body.

Q5. How does a bounded type parameter affect type erasure at the bytecode level?

Without a bound, a type parameter T erases to Object. With a bound, it erases to the first bound in the list. <T extends Number> erases to Number, so the compiled bytecode uses Number wherever T appears in the class or method. <T extends Number & Comparable<T>> also erases to Number (the class bound is first), while methods from Comparable are accessed via a bridge cast (Comparable) value in the bytecode. This is why the order of bounds matters practically: placing the most commonly accessed type first avoids an extra cast instruction at every call to its methods.

Q6. When should you use a bounded type parameter versus writing a concrete type in a method signature?

Use a bounded type parameter when the same algorithmic logic should work for multiple types that share a common contract. <T extends Number> double average(List<T>) works for List<Integer>, List<Double>, and List<Long> — one method, three or more types. Writing double average(List<Number>) forces callers to use Number exactly and prevents passing a List<Integer> directly (due to invariance of generic types). The bounded version is both more general (accepts subtypes) and more type-safe (preserves the element type throughout). Use a concrete type only when exactly that type — and no other — is the right input.

FAQs

Can a bound use a parameterized type like Comparable instead of Comparable?

Yes, <T extends Comparable<String>> compiles, but it is rarely the right choice. It means compareTo() must accept String specifically, so only types that implement Comparable<String> (which is just String itself) would satisfy the bound. The self-referential form <T extends Comparable<T>> is almost always what you want because it says "T compares against its own type," which applies to String, Integer, LocalDate, and any self-comparable type.

Can you have a lower bound on a type parameter, like ?

No. Type parameter bounds can only be upper bounds — <T extends SomeType>. Lower bounds (super) are only available on wildcards (? super Integer), not on type parameters (T). This asymmetry is a deliberate design decision in Java's type system. The distinction between type parameters and wildcards is important: a type parameter introduces a named variable (T), while a wildcard describes an unknown type at a usage site (?).

What happens if you pass null to a method with a bounded type parameter?

null satisfies any reference type bound — you can pass null wherever T is expected, regardless of bounds. The method's body will encounter null at runtime, and calling any method on it (like item.doubleValue() in a <T extends Number> method) will throw NullPointerException. Bounded type parameters do not add null safety; if null handling matters, the method body should check explicitly with a null guard.

Can a type variable be used as a bound for another type variable in the same declaration?

Yes. <T, S extends T> declares two type variables where S must be a subtype of T. This pattern appears in some generic method signatures where the relationship between two type parameters needs to be expressed. For example, <T, S extends T> List<T> merge(List<T> first, List<S> second) says the second list's element type must be a subtype of the first list's element type, so elements from either can be placed in a List<T>.

Does a bounded type parameter change the behavior of the instanceof operator inside the method?

Not directly — instanceof T is still a compile error regardless of bounds, because T is erased at runtime. Inside a method bounded to <T extends Number>, item instanceof Number is valid and returns true for any T value (since the bound guarantees it). item instanceof Integer is also valid and checks the actual runtime class, which is useful when writing code that needs to distinguish between Integer, Double, and Long at runtime even though all satisfy the Number bound.

Can a type parameter have a bound that refers to another type parameter declared in the same signature?

Yes — <T, S extends T> declares S as a subtype of T, with both parameters declared in the same method or class. This expresses a relationship between two independent type variables: whatever T turns out to be, S must be a subtype of it. This pattern appears in utility methods that need to express "copy from a more specific type into a more general container" — for example, a method that copies elements from a List<S> into a List<T> where S extends T.

Summary

A bounded type parameter is a restriction and an expansion at the same time. <T extends SomeType> tells callers which types are acceptable and tells the compiler which methods are safe to call on values of type T inside the body. The bound is what connects the generic declaration to real method calls — without it, only Object methods are available; with it, every method declared on the bound becomes callable without a cast.

Three forms appear most in production code. Single upper bounds — <T extends Number> — are for numeric processing utilities that should work across Integer, Double, Long, and other numeric types. Multiple bounds — <T extends Number & Comparable<T>> — combine a class and one or more interfaces when the method needs guarantees from more than one contract simultaneously. Recursive bounds — <T extends Comparable<T>> — appear everywhere sorting and ordering are needed, expressing that T knows how to compare itself against another T.

Erasure ties everything together: the first bound is what T becomes in bytecode, which determines which methods are directly accessible and which require bridge casts. Keeping the class bound first when combining bounds is both the convention and the slightly more efficient choice.

What to Read Next