Bounded Types and Wildcards in Java

Bounds restrict what a type parameter may be, and wildcards let a method accept a family of parameterised types safely.

Bounded type parameters

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

Without the bound, T would be treated as Object and doubleValue() would not compile. A bound both restricts what callers may pass and grants access to the members of the bound.

total(List.of(1, 2, 3));          // Integer extends Number, fine
total(List.of(1.5, 2.5));         // Double, fine
// total(List.of("a", "b"));      // compile error

Multiple bounds

public static <T extends Comparable<T> & Serializable> T largest(List<T> values) {
    T best = values.get(0);
    for (T value : values) {
        if (value.compareTo(best) > 0) {
            best = value;
        }
    }
    return best;
}

Use & to require several bounds. A class bound, if there is one, must come first; the rest are interfaces. Note that extends is used for interfaces here too.

Wildcards

A wildcard ? stands for "some unknown type". It appears in the use of a generic type, not in its declaration.

Unbounded wildcard

public static void printAll(Collection<?> items) {
    for (Object item : items) {       // safe: everything is an Object
        System.out.println(item);
    }
    // items.add("x");                // not allowed: the element type is unknown
}
printAll(List.of("a", "b"));
printAll(Set.of(1, 2, 3));            // both accepted

Upper bounded wildcard: extends

public static double sum(List<? extends Number> values) {
    double total = 0;
    for (Number value : values) {     // reading as Number is safe
        total += value.doubleValue();
    }
    return total;
}
sum(List.of(1, 2, 3));          // List<Integer>
sum(List.of(1.5, 2.5));         // List<Double>
// values.add(1);               // still not allowed inside the method

You can read, but you cannot write. The list might be a List<Double>, so adding an Integer would corrupt it.

Lower bounded wildcard: super

public static void addNumbers(List<? super Integer> target) {
    target.add(1);
    target.add(2);
    // Integer first = target.get(0);   // not allowed: only Object is guaranteed
}
addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Number>());
addNumbers(new ArrayList<Object>());     // all accepted

You can write, but reading gives you only Object. The list is at least able to hold integers, so adding one is always safe.

PECS: Producer Extends, Consumer Super

The parameterUseYou can
Produces values you read? extends TRead as T, not write
Consumes values you write? super TWrite T, read only as Object
BothPlain TBoth
public static <T> void copy(List<? extends T> source, List<? super T> target) {
    for (T item : source) {       // source produces
        target.add(item);          // target consumes
    }
}
List<Integer> numbers = List.of(1, 2, 3);
List<Object> destination = new ArrayList<>();
copy(numbers, destination);        // accepted, and type safe
PECS is the rule to memorise. If a parameter is a source of data use extends; if it is a destination use super. The standard library follows it everywhere, which is why Collections.copy and Stream.map have the signatures they do.

Where you meet it in the library

// Comparator: consumes T, so super
list.sort(Comparator<? super String> comparator);

// Stream.map: the mapper consumes T and produces R
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

// forEach: the action consumes T
void forEach(Consumer<? super T> action);

These signatures look intimidating until PECS makes them obvious: every parameter is marked by whether it produces or consumes.

Wildcard compared with a type parameter

// A wildcard: simplest when the type is used once
public static int size(Collection<?> items) {
    return items.size();
}

// A type parameter: needed when the type appears more than once
public static <T> void swapEnds(List<T> items) {
    T first = items.get(0);
    items.set(0, items.get(items.size() - 1));
    items.set(items.size() - 1, first);
}

Use a wildcard when the unknown type appears in exactly one place. Use a named parameter when two positions must agree.

Common mistakes

  • Trying to add to a List<? extends T>.
  • Expecting to read a specific type from a List<? super T>.
  • Writing List<?> where List<Object> was meant. They are different: List<Object> accepts writes, List<?> does not.
  • Using a wildcard in a return type, which pushes the awkwardness onto every caller.
  • Using super where extends was needed, and finding that reads no longer type check.

Best practices

  • Apply PECS to every generic parameter of a public method.
  • Prefer a wildcard for a type used once, a named parameter otherwise.
  • Avoid wildcards in return types.
  • Add a bound when the method needs members of a particular type.
  • Keep null in mind: it is the only value assignable to a ? extends T position.

Practice

  1. Write a method that sums any Collection of numbers, and explain the wildcard you chose.
  2. Why is list.add(1) rejected for List<? extends Number> but accepted for List<? super Integer>?
  3. Explain the signature of Stream.map using PECS.
  4. Give an example where List<?> and List<Object> behave differently.
  5. Write a copyAll method with the correct bounds and test it with a list of integers into a list of objects.

Conclusion

Bounds tell the compiler what a type parameter can do; wildcards let a method accept a family of parameterised types. Remember PECS, and most generic signatures in the standard library become readable.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Generics in Java

Generics let a class or method work with a type supplied by the caller, moving whole families of errors from runtime to compile time.

Read more
Java

Introduction to Java

Java is a statically typed, object oriented language that compiles to bytecode and runs on a virtual machine, which is what makes it portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.