Concepts / Type Parameters and Their Limitations

Type Parameters and Their Limitations

A type parameter that can be any type limits you to operations that every type supports.

  • Programming

Where Flexibility Meets a Wall

Generic classes and generic methods are written around a type parameter, usually named T, that stands in for some type chosen later. That flexibility is the whole point. One method can work on integers, strings, or any other type without being rewritten. But flexibility has a cost, and this article is about the moment that cost shows up.

Suppose you want a generic method that compares two of its inputs. You would expect the same code to work for every type that supports comparison. Yet the techniques covered so far do not let you write that method. Before any new tool is introduced, it is worth understanding exactly why. We will trace what the code is allowed to do with T at each stage, the way you would step through a program and watch its state change.

Tracing What an Unrestricted T Allows

When a type parameter can be any type, the code inside the generic class or method has to work no matter which type is substituted for T. That means the code may only rely on what every possible type supports. Nothing more. The compiler enforces this: if the body of the method calls an operation that is not guaranteed for every type, the call is rejected.

The operation that fails in the comparison method is compareTo(). It is defined in the Comparable interface and is not defined for all objects. An unrestricted type parameter therefore cannot rely on it. The diagram below contrasts the two situations: what the code may rely on when T is unrestricted, and what it may rely on once T is restricted.

Operations everytype supportsallowedOperations everytype supportsallowedcompareTo()not guaranteed, rejectedcompareTo()guaranteed, usabledoes not extend toalso includes
Which operations can code call on a type parameter T that could be any type, and which ones are rejected by the compiler?

Watching a Compare Method Fail

Consider the simplest generic method that tries to return the larger of two values. The intent is clear, but the body depends on comparing two values of type T. The code below is an illustrative example written for this article, not taken from the source. It shows the shape of the problem.

java

What do you think happens?

Will this method compile when T has no bound?

  • Yes, because every type has compareTo()

  • No, because compareTo() is not guaranteed for an unrestricted T

Reveal answer
Answer:

No, because compareTo() is not guaranteed for an unrestricted T

The source states that compareTo() is defined in the Comparable interface and is not defined for all objects, so an unrestricted type parameter cannot rely on it. The compiler rejects the call.

Restricting T with Comparable

The fix is to restrict the type parameter. Restricting T to Comparable tells the compiler that the generic code applies only to comparable objects. Once that promise is made, the body of the method is free to call compareTo(), because every type the method can be used with is known to provide it. The declaration below shows how the pieces of the restriction map to what the code is allowed to do.

<TT is a type parameterextendsT must satisfy the boundthat followsComparable<T>>the bound: T must becomparable, unlockingcompareTo()
How do the pieces of the declaration <T extends Comparable<T>> map to what the generic code is allowed to do with T?

Tracing the Method Before and After the Restriction

Now trace the same method after the restriction is added. Only the declaration line changes. The body is identical, and the call to compareTo() is now permitted because the bound guarantees it. The example is again generated for this article to illustrate the change.

Before
static <T> T maxOf(T a, T b) {
    if (a.compareTo(b) >= 0) {
        return a;
    }
    return b;
}

T is unrestricted, so compareTo() is not guaranteed and the call is rejected

After
static <T extends Comparable<T>> T maxOf(T a, T b) {
    if (a.compareTo(b) >= 0) {
        return a;
    }
    return b;
}

T is restricted to Comparable, so compareTo() is guaranteed and the call is allowed

Worked example

Checking whether a call is allowed

Inside a generic method with type parameter T, you want to call compareTo() on a variable of type T. Is the call allowed?

  1. 1
    Identify the bound on T

    If T has no bound, the code may only rely on operations that every type supports.

  2. 2
    Check whether compareTo() is guaranteed

    compareTo() is defined in the Comparable interface and is not defined for all objects, so it is not guaranteed for an unrestricted T.

  3. 3
    Add the restriction

    Restricting T to Comparable tells the compiler the code applies only to comparable objects, so compareTo() becomes usable.

Result

Unbounded: not allowed. Bounded by Comparable: allowed.

The Trade-off Between Generality and Capability

Restriction is a trade. The type parameter now covers fewer types, which is a loss of generality. In return, the code can use operations that are guaranteed by the restriction, and compareTo() is the central example. The concept map below shows why this works: the set of types that implement Comparable sits inside the set of all types, and restricting T to that subset is what unlocks compareTo().

All typesunrestricted T can be anyof theseComparable typestypes that implementComparableT restricted toComparablecovers fewer typescompareTo()guaranteed inside therestricted bodycontainsrestricts T toguarantees
How does the set of types that implement Comparable sit inside the set of all types, and why does restricting T to that subset unlock compareTo()?

Mistakes When Writing Bounded Generic Code

  • Common mistake

    Expecting compareTo() to work on any T without a bound

    What it looks like
    static <T> T maxOf(T a, T b) that calls a.compareTo(b) in its body
    Why it fails

    compareTo() is defined in the Comparable interface and is not defined for all objects, so an unrestricted T cannot rely on it.

    Do this instead

    Restrict the type parameter to Comparable so that every type the method can be used with provides compareTo().

  • Common mistake

    Assuming a restriction costs nothing

    Why it fails

    Restriction is a trade. The type parameter covers fewer types, which is a loss of generality.

    Do this instead

    Add a bound only when the body genuinely needs an operation that the bound guarantees, and accept that fewer types can be passed in.

Checking Your Understanding of the Bound

EASY

A generic method declares its type parameter as <T> with no bound and then calls compareTo() on a value of type T. Explain why the compiler rejects the call, and state the change to the declaration that makes the call usable.

Hints
  • Ask which interface defines compareTo() according to the source.
  • Think about what the bound promises to the compiler about the types that can be substituted for T.

Key Takeaways

  • A type parameter that can be any type limits the code to operations that every type supports.
  • compareTo() is defined in the Comparable interface and is not defined for all objects, so an unrestricted type parameter cannot rely on it.
  • Restricting T to Comparable, written as <T extends Comparable<T>>, tells the compiler the code applies only to comparable objects and makes compareTo() usable inside the body.
  • Restriction is a trade: the code gains guaranteed operations, but the type parameter covers fewer types.