Concepts / Generic Classes and Methods

Generic Classes and Methods

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

  • Programming

Why Flexibility Hits 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 it shows up the moment a generic method needs to compare 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. The reason is worth understanding before any new tool is introduced.

What do you think happens?

Can the body of a generic method call compareTo() on a value of type T when T has no restriction at all?

  • Yes, every type has compareTo()

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

Reveal answer
Answer:

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

compareTo() is defined in the Comparable interface and is not defined for all objects. An unrestricted type parameter therefore cannot rely on it, because it may stand for a type that has no such method.

What an Unrestricted Type Parameter Can Use

A type parameter with no restriction is a promise that the code works for any type. The only safe assumption the code can make about a value of that type is that it supports the operations every type supports. Any operation that belongs only to some types is outside that promise, and the code may not use it. The source describes this directly: a type parameter that can be any type limits you to operations that every type supports.

Every-type operationsEvery-type operationscompareTo()not guaranteedcompareTo()guaranteed
Which operations can code call on a value of type T when T is unrestricted, and how does that differ from T restricted to Comparable?

The two columns in the diagram are two different contracts with the code. In the unrestricted column, the code may only rely on what every possible type supports, so the every-type operations are available but compareTo() is not. In the restricted column, the code may also rely on the operations that belong to Comparable, which is the only route to compareTo() that the source describes.

Tracing a Generic Method That Compares

Suppose you want a generic method that returns the larger of two values. The natural first attempt uses compareTo() on the first argument. Read the code below and trace what happens to the value a when the method runs. The variable a has the type T, and T could be any type, so the compiler has no guarantee that a has a compareTo() method.

java

The correct comparison logic is the same. Only the declaration of T has to change, so that the body is allowed to call compareTo(). The next section shows how that declaration works.

Bounding T with Comparable

Restricting a type parameter to Comparable tells the compiler that the generic code applies only to comparable objects. The restriction is written as a bound on the type parameter, for example <T extends Comparable<T>>. Once that bound is in place, the body of the generic method or class is free to call compareTo(), because every type that the method can be used with is known to provide it.

Definition

A bounded type parameter is a type parameter that is restricted to types meeting a stated condition. Here, T extends Comparable<T> restricts T to types that the Comparable interface describes, which makes the methods of that interface, including compareTo(), usable on values of type T.

Comparabledefines compareTo()Tlimited to comparable typesrestricts T to
How does declaring T extends Comparable place T under the Comparable interface, so that calling compareTo() on a T value is guaranteed to be valid?
Call compareTo() onTinside generic codeIs T bounded byComparable?Call allowedCompile errorrejected before runningyesno
What check does the compiler perform when code calls compareTo() on a T, and why is the call rejected before the program runs when T has no bound?

Tracing the Restricted Version

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

T is unrestricted, so compareTo() is not guaranteed on a

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

T extends Comparable<T>, so compareTo() is guaranteed on a

Trace the restricted version step by step. The method receives a and b, both of type T. Because T is bounded by Comparable, the call a.compareTo(b) is valid for any types the method can be used with. The result is zero or positive when a is not smaller than b, so the method returns a. Otherwise it returns b. Only the declaration changed, and the body is the same as the rejected version.

java
Output
pear
7
Notes on the code
  • The String and Integer values both satisfy the Comparable bound
  • largest(3, 7) returns 7 because 3 is smaller than 7
  • largest("apple", "pear") returns pear because pear sorts after apple

The Cost of Restriction

Restriction is a trade. The type parameter now covers fewer types, which is a loss of generality. In return, the code gains operations that are guaranteed by the restriction, and compareTo() is the central example. A method that was usable with any type becomes usable only with types that are comparable.

Mistakes Beginners Make

  • Common mistake

    Assuming compareTo() works on any T

    What it looks like
    public static <T> T largest(T a, T b) { if (a.compareTo(b) >= 0) { return a; } return b; }
    Why it fails

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

    Do this instead

    Declare the bound, as in <T extends Comparable<T>>, so the body can call compareTo().

  • Common mistake

    Treating the bound as free

    Why it fails

    A bound narrows the type parameter to fewer types, so the method can no longer be used with every type it once accepted.

    Do this instead

    Add only the restriction the body needs, and check which types callers can still pass.

  • Common mistake

    Forgetting that the bound belongs in the declaration of T rather than in the method body

Predicting the Restriction in Practice

MEDIUM

A generic method sorts nothing and only needs to decide whether one value of type T is smaller than another. Write the declaration of the type parameter that allows the body to call compareTo(), and explain in one sentence what the bound gives you and what it costs you.

Hints
  • Look at which interface defines compareTo()
  • The bound should be written on T in the method declaration
  • The cost is that T now covers fewer types

Key Takeaways

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