Generics

exercises
lab
java
generics
wildcards

Practical for 2026-05-21-correct-programming (Week 12). Extends java-generics (Week 9) with practice designing generic classes and using bounded wildcards.

Setup

A simple Bus domain models a class hierarchy of things that can ride a bus:

Bus <>--- Passenger
              ^
      +-------+-------+
      |               |
     Pet             Person
                       ^
              +--------+--------+
              |                 |
       TransportWorker   GeneralPublic
              ^                 ^
      +-------+-------+         |
      |               |         |
MaintenanceStaff   BusDriver  Concession

Passenger is the common supertype every rider implements/extends; Pet and Person are its two direct subtypes; TransportWorker (with subtypes MaintenanceStaff, BusDriver) and GeneralPublic (with subtype Concession) are both Persons. A Bus holds a collection of Passengers up to some capacity.

§1 Generic Bus

Task 0. The starting Bus class is not generic — it stores plain Passengers, so a single Bus instance could hold a mix of Pets and BusDrivers with no way to restrict it further:

public class Bus {
    private final List<Passenger> passengers;
    private final int capacity;

    public Bus(int capacity) {
        this.capacity = capacity;
        this.passengers = new ArrayList<>();
    }

    /**
     * @requires passenger != null
     * @ensures passenger is added to this bus and true is returned,
     *          unless this bus is already at capacity, in which case
     *          nothing changes and false is returned
     */
    public boolean addPassenger(Passenger passenger) {
        if (passengers.size() >= capacity) {
            return false;
        }
        passengers.add(passenger);
        return true;
    }

    public List<Passenger> getPassengers() {
        return passengers;
    }
}

Rewrite Bus to be generic, so a single Bus instance can be restricted to carrying just one kind of passenger (e.g. Bus<Pet>, Bus<BusDriver>).

public class Bus<T> {
    private final List<T> passengers;
    private final int capacity;

    public Bus(int capacity) {
        this.capacity = capacity;
        this.passengers = new ArrayList<>();
    }

    /**
     * @requires passenger != null
     * @ensures passenger is added to this bus and true is returned,
     *          unless this bus is already at capacity, in which case
     *          nothing changes and false is returned
     */
    public boolean addPassenger(T passenger) {
        if (passengers.size() >= capacity) {
            return false;
        }
        passengers.add(passenger);
        return true;
    }

    public List<T> getPassengers() {
        return passengers;
    }
}

Task 1. As written, Bus<T> accepts any type argument at all — including types with nothing to do with Passenger (e.g. Bus<String>). Add a bound to T that restricts it to Passenger and its subtypes.

public class Bus<T extends Passenger> {
    // ...unchanged from Task 0...
}

Bounding T extends Passenger restricts Bus’s type argument to Passenger or one of its subtypes (Pet, Person, TransportWorker, …), while a particular Bus instance still only carries one such type (e.g. Bus<Pet> can’t also hold a BusDriver). It also means code inside Bus<T> could safely call any Passenger method directly on a T value, if needed.

Task 2. Write a NoPetsBus<T> subclass of Bus<T> that statically (at compile time, not with a runtime check) excludes Pet as a valid type argument.

public class NoPetsBus<T extends Person> extends Bus<T> {
    public NoPetsBus(int capacity) {
        super(capacity);
    }
}

Bounding the subclass’s type parameter to T extends Person (rather than Passenger) is stricter than Bus’s own bound. Since Pet is a Passenger but not a Person, NoPetsBus<Pet> simply doesn’t compile — the exclusion is enforced entirely by the type system, with no instanceof checks needed anywhere.

§2 Bus Stop

Task 3. Write a trainingBus method that takes a Bus<BusDriver> and a list of TransportWorkers, and adds every BusDriver among them to the bus (stopping early if the bus becomes full):

public static void trainingBus(Bus<BusDriver> bus, List<? extends TransportWorker> trainees) {
    ...
}
/**
 * @requires bus != null && trainees != null
 * @ensures every BusDriver in trainees (in order) is added to bus,
 *          stopping as soon as bus is full
 */
public static void trainingBus(Bus<BusDriver> bus, List<? extends TransportWorker> trainees) {
    for (TransportWorker trainee : trainees) {
        if (trainee instanceof BusDriver busDriver) {
            boolean added = bus.addPassenger(busDriver);
            if (!added) {
                return;
            }
        }
    }
}

trainees only needs to be read from (never written to), so it takes the upper-bounded wildcard List<? extends TransportWorker> — this lets callers pass a List<TransportWorker>, List<BusDriver>, or List<MaintenanceStaff> alike, at the cost of the compiler only guaranteeing each element is at least a TransportWorker (hence the instanceof check before adding).

Task 4. Write a transferStudents method that moves every passenger out of a Bus<Concession> and into another bus that’s allowed to carry Concession passengers (or any of their supertypes):

public static void transferStudents(Bus<Concession> from, Bus<? super Concession> to) {
    ...
}
/**
 * @requires from != null && to != null
 * @ensures passengers are moved from `from` into `to`, in order, until either
 *          `from` is empty or `to` is full (any remaining passengers stay in `from`)
 */
public static void transferStudents(Bus<Concession> from, Bus<? super Concession> to) {
    Iterator<Concession> iterator = from.getPassengers().iterator();
    while (iterator.hasNext()) {
        Concession passenger = iterator.next();
        boolean added = to.addPassenger(passenger);
        if (!added) {
            return;
        }
        iterator.remove();
    }
}

to only needs to be written to, so it takes the lower-bounded wildcard Bus<? super Concession> — this lets callers pass a Bus<Concession>, Bus<GeneralPublic>, Bus<Person>, or Bus<Passenger> alike, since all of them can legally accept a Concession passenger via addPassenger.

Task 5. transferStudents only works for exactly Bus<Concession> as its source. Generalise it into a fully generic method that works for Bus<T> for any T that’s at least a Concession.

/**
 * @requires from != null && to != null
 * @ensures passengers are moved from `from` into `to`, in order, until either
 *          `from` is empty or `to` is full (any remaining passengers stay in `from`)
 */
public static <T extends Concession> void transferAllConcession(Bus<T> from, Bus<? super T> to) {
    Iterator<T> iterator = from.getPassengers().iterator();
    while (iterator.hasNext()) {
        T passenger = iterator.next();
        boolean added = to.addPassenger(passenger);
        if (!added) {
            return;
        }
        iterator.remove();
    }
}

Introducing the type parameter <T extends Concession> on the method itself (rather than fixing T = Concession) means from can be Bus<Concession> or any more specific subtype-bus (e.g. a hypothetical Bus<StudentConcession>), while to’s wildcard bound ? super T is resolved relative to whatever T the call site infers — transferStudents from Task 4 is just the special case where T is fixed to Concession.