Bounded Type Parameters in Java Generics

Introduction A plain type parameter such as <T> accepts any type - which means that, inside the generic code, you can only use the methods every object has. To find the largest of a list of values, you need to compare them; to add up a list of numbers, you need them to be numbers. Bounded type parameters restrict which types can be used, and in return let you call the methods of the bound. This tutorial explains bounded type parameters in Java generics. We will start with the problem an unbounded type parameter causes. We will then use upper bounds with extends, for both classes and interfaces. Next we will use the most common bound of all, <T extends Comparable<T>>, and combine several bounds. Lastly, we will use bounds on generic classes, and see how bounded type parameters differ from wildcards.

(Note - This tutorial builds on Generic Classes, Interfaces and MethodsClick to read tutorial: Java Generic Classes, Interfaces and Methods.) The problem with an unbounded type parameter Inside a method with a type parameter <T>, the compiler knows nothing about T except that it is some kind of Object. So a method which tries to add up numbers or compare values does not compile -
An unbounded type parameter has only Object's methods
//Unbounded.java
import java.util.List;

public class Unbounded {
  static <T> double sum(List<T> values) {
    double total = 0;
    for (T v : values) {
      total += v.doubleValue();
    }
    return total;
  }
}
 OUTPUT on compiling the above code
Unbounded.java:8: error: cannot find symbol
      total += v.doubleValue();
                ^
  symbol:   method doubleValue()
  location: variable v of type T
  where T is a type-variable:
    T extends Object declared in method <T>sum(List<T>)
1 error
doubleValue() is a method of Number, and T might be a String or anything else. We need to tell the compiler that T is always a Number. Upper bounds with extends Syntax of a bounded type parameter
<T extends Bound>
Where,
  • T can only be Bound itself, or a subtype of it.
  • extends is used for both classes and interfaces - in generics there is no implements. <T extends Comparable<T>> means "T implements Comparable".
  • inside the generic code, every method of Bound can be called on values of type T.
A bounded type parameter: T extends Number
//BoundedSum.java
import java.math.BigDecimal;
import java.util.List;

public class BoundedSum {
  static <T extends Number> double sum(List<T> values) {
    double total = 0;
    for (T v : values) {
      total += v.doubleValue();       // allowed: T is a Number
    }
    return total;
  }

  static <T extends Number> double average(List<T> values) {
    return values.isEmpty() ? 0 : sum(values) / values.size();
  }

  public static void main(String[] args) {
    List<Integer> counts = List.of(3, 7, 5);
    List<Double> prices = List.of(19.99, 5.25, 12.50);
    List<BigDecimal> amounts = List.of(new BigDecimal("100.10"), new BigDecimal("0.40"));
    System.out.println("sum of counts    = " + sum(counts));
    System.out.println("average of prices = " + average(prices));
    System.out.println("sum of amounts   = " + sum(amounts));
  }
}
 OUTPUT of the above code
sum of counts    = 15.0
average of prices = 12.579999999999998
sum of amounts   = 100.5
Integer, Double and BigDecimalClick to read tutorial: BigInteger and BigDecimal in Java with Examples are all subclasses of Number, so one method handles all three. Passing a List<String> is now a compile error -
A type argument outside the bound
//BoundViolation.java
import java.util.List;

public class BoundViolation {
  static <T extends Number> double sum(List<T> values) {
    return 0;
  }

  public static void main(String[] args) {
    sum(List.of("one", "two"));
  }
}
 OUTPUT on compiling the above code
BoundViolation.java:10: error: method sum in class BoundViolation cannot be applied to given types;
    sum(List.of("one", "two"));
    ^
  required: List<T>
  found:    List<String>
  reason: inference variable E has incompatible bounds
    upper bounds: Number,Object
    lower bounds: String
  where T,E are type-variables:
    T extends Number declared in method <T>sum(List<T>)
    E extends Object declared in method <E>of(E,E)
1 error
The most common bound - T extends Comparable<T> To find a maximum, sort, or check order, generic code needs to compare values. Types with a natural order - String, Integer, LocalDate and many more - implement Comparable<T>, whose compareTo() method returns a negative number, zero or a positive number (see compareTo() for stringsClick to read tutorial: Java String Comparison: == vs equals() vs compareTo()). Bounding T by Comparable<T> lets us call compareTo().
Finding the maximum with T extends Comparable
//MaxFinder.java
import java.time.LocalDate;
import java.util.List;

public class MaxFinder {
  static <T extends Comparable<T>> T max(List<T> values) {
    if (values.isEmpty()) {
      throw new IllegalArgumentException("Cannot find the max of an empty list");
    }
    T best = values.get(0);
    for (T v : values) {
      if (v.compareTo(best) > 0) {
        best = v;
      }
    }
    return best;
  }

  static <T extends Comparable<T>> boolean isSorted(List<T> values) {
    for (int i = 1; i < values.size(); i++) {
      if (values.get(i - 1).compareTo(values.get(i)) > 0) return false;
    }
    return true;
  }

  static <T extends Comparable<T>> T clamp(T value, T low, T high) {
    if (value.compareTo(low) < 0) return low;
    if (value.compareTo(high) > 0) return high;
    return value;
  }

  public static void main(String[] args) {
    System.out.println("max number : " + max(List.of(42, 7, 99, 15)));
    System.out.println("max name   : " + max(List.of("Tom", "Nancy", "Harry")));
    System.out.println("latest date: " + max(List.of(LocalDate.of(2026, 3, 1), LocalDate.of(2026, 10, 28))));
    System.out.println("sorted?    : " + isSorted(List.of(1, 3, 3, 8)) + ", " + isSorted(List.of("b", "a")));
    System.out.println("clamp      : " + clamp(150, 0, 100) + ", " + clamp("m", "a", "f"));
  }
}
 OUTPUT of the above code
max number : 99
max name   : Tom
latest date: 2026-10-28
sorted?    : true, false
clamp      : 100, f
Explanation of the code
  • The bound T extends Comparable<T> reads as "T can be compared with other Ts". The same max() worked for integers, strings and dates.
  • The return type is T, not Comparable, so the caller gets back the exact type it passed in - an Integer, a String or a LocalDate - with no cast.
  • The library's own version, Collections.max(), is declared as <T extends Object & Comparable<? super T>>. The ? super T part is a wildcard, which makes it work for even more types - see wildcards and PECSClick to read tutorial: Java Generics Wildcards: extends, super and PECS.
Multiple bounds A type parameter can have several bounds, joined with &. T must then satisfy all of them. If one bound is a class, it must come first, followed by any number of interfaces.
A type parameter with two bounds
//MultipleBounds.java
import java.util.List;

public class MultipleBounds {
  interface Named { String name(); }
  interface Priced { double price(); }

  record Book(String name, double price) implements Named, Priced { }
  record Course(String name, double price, int hours) implements Named, Priced { }

  static <T extends Named & Priced> void printPriceList(List<T> items) {
    double total = 0;
    for (T item : items) {
      System.out.printf("  %-24s %8.2f%n", item.name(), item.price());   // methods of both bounds
      total += item.price();
    }
    System.out.printf("  %-24s %8.2f%n", "Total", total);
  }

  public static void main(String[] args) {
    printPriceList(List.of(new Book("Effective Java", 45.0), new Book("Java Concurrency", 52.5)));
    printPriceList(List.of(new Course("Java Fundamentals", 99.0, 40)));
  }
}
 OUTPUT of the above code
  Effective Java              45.00
  Java Concurrency            52.50
  Total                       97.50
  Java Fundamentals           99.00
  Total                       99.00
Inside printPriceList(), both name() and price() can be called, because T is known to be both Named and Priced. (The examples use Java 16+ records, compact classes for data, which can implement interfaces like any class.) Bounds on generic classes Bounds work on a class's type parameters too. Every use of the class must then supply a type within the bound, and every method of the class can rely on it.
A generic class with a bounded type parameter
//Statistics.java
import java.util.List;

public class Statistics<T extends Number & Comparable<T>> {
  private final List<T> values;

  Statistics(List<T> values) { this.values = values; }

  T min() {
    T result = values.get(0);
    for (T v : values) if (v.compareTo(result) < 0) result = v;
    return result;
  }

  double mean() {
    double sum = 0;
    for (T v : values) sum += v.doubleValue();
    return sum / values.size();
  }

  public static void main(String[] args) {
    Statistics<Integer> marks = new Statistics<>(List.of(72, 85, 64, 90));
    System.out.println("min = " + marks.min() + ", mean = " + marks.mean());
    Statistics<Double> temps = new Statistics<>(List.of(21.5, 19.0, 23.25));
    System.out.println("min = " + temps.min() + ", mean = " + temps.mean());
  }
}
 OUTPUT of the above code
min = 64, mean = 77.75
min = 19.0, mean = 21.25
Statistics<String> would not compile, because String is not a Number. Bounded type parameters vs wildcards Generics have a second way of restricting types: wildcards, such as List<? extends Number>. The difference is -
  • A bounded type parameter gives the type a name, T, which can be used several times - to link a parameter to the return type, or two parameters to each other, as in T max(List<T> values).
  • A wildcard is an unnamed "some type", used when the type appears only once and does not need to be referred to again - as in double sum(List<? extends Number> values).
sum() above could be written either way; max() needs the named T, because it returns one of the elements. Wildcards are covered next. Summary In this tutorial we first saw that an unbounded type parameter only offers the methods of Object. We then used upper bounds with extends, such as <T extends Number>, to call doubleValue() and reject other types at compile time. Next we used the most common bound, <T extends Comparable<T>>, to write max(), isSorted() and clamp() for any comparable type. We then combined bounds with &, put bounds on a generic class, and lastly compared bounded type parameters with wildcards. In the next tutorial, we will look at wildcards, extends, super and the PECS ruleClick to read tutorial: Java Generics Wildcards: extends, super and PECS.

Bounded Type Parameters in Java Generics - JavaBrahman