Java Arena
Step 1 of 4

Hierarchies: one object, many types

Modules 34 and 35 gave classes two ways to be related: a class extends one superclass, and it can implement any number of interfaces. Together, the classes and interfaces of a program form a hierarchy, a family tree of types. This module looks at what that means for one object, and for programs that handle many kinds of objects at once. The idea has a name, polymorphism, from the Greek for "many forms": one object can be used as several types, and one variable can refer to objects of many classes.

A toll booth's vehicles

A toll booth charges every vehicle that goes through. A plain Vehicle, such as a motorbike, pays 3. A Car pays 6. A Taxi is a car, and pays 2 less than other cars. A Truck pays 5 for each axle. Taxis and trucks work for companies, which get a bill every month, so both implement an interface, Commercial, with one method: company(). Here's the hierarchy, with each class under the one it extends:

Object
  Vehicle
    Car
      Taxi     implements Commercial
    Truck      implements Commercial

Vehicle doesn't say what it extends, so it extends Object (module 34). Here's the program, with one taxi in five variables:

Main.java
public class Main {
    public static void main(String[] args) {
        Taxi taxi = new Taxi("TX-12", "Metro Cabs");
        Car car = taxi;
        Vehicle vehicle = taxi;
        Commercial customer = taxi;
        Object thing = taxi;

        System.out.println(taxi.toll() + " " + car.toll() + " " + vehicle.toll());
        System.out.println(customer.company());
        System.out.println(thing);
        System.out.println(thing == customer);
    }
}
Vehicle.java
public class Vehicle {
    private String plate;

    public Vehicle(String plate) {
        this.plate = plate;
    }

    public String getPlate() {
        return this.plate;
    }

    public int toll() {
        return 3;
    }

    @Override
    public String toString() {
        return this.plate + " pays " + this.toll();
    }
}
Car.java
public class Car extends Vehicle {
    public Car(String plate) {
        super(plate);
    }

    @Override
    public int toll() {
        return 6;
    }
}
Taxi.java
public class Taxi extends Car implements Commercial {
    private String company;

    public Taxi(String plate, String company) {
        super(plate);
        this.company = company;
    }

    @Override
    public int toll() {
        return super.toll() - 2;
    }

    @Override
    public String company() {
        return this.company;
    }
}
Truck.java
public class Truck extends Vehicle implements Commercial {
    private String company;
    private int axles;

    public Truck(String plate, String company, int axles) {
        super(plate);
        this.company = company;
        this.axles = axles;
    }

    @Override
    public int toll() {
        return 5 * this.axles;
    }

    @Override
    public String company() {
        return this.company;
    }
}
Commercial.java
public interface Commercial {
    String company();
}
Output
4 4 4
Metro Cabs
TX-12 pays 4
true
  • There's one new, so there's one object, a Taxi. Five variables refer to it, and each has a different type.
  • Each assignment compiles because a Taxi is each of those things: a Car, since Taxi extends Car; a Vehicle, since Car extends Vehicle; a Commercial, since Taxi implements it; and an Object, as every object is.
  • Putting the object in another variable doesn't change it or copy it. thing == customer is true: both refer to the same Taxi.
  • The toll is 4 through every variable. The object's class decides which version of a method runs (module 34), and this object is a Taxi, whatever the variable's type.

Every type an object has

The taxi has five types. In general, an object's types are:

  • its own class,
  • every class above it, all the way up to Object,
  • every interface that its class, or any class above it, implements,
  • and every interface above those, all the way up, since an interface can extend another one, as Set extends Collection (module 35).

A variable of any of those types can refer to the object. The last two rules mean that interfaces are inherited, like methods: a class that extends Taxi is Commercial too, without writing implements again, and an ArrayList, which implements List, is a Collection too.

What changes from variable to variable is what you may call through it, since the variable's type decides that (modules 34 and 35):

Through a variable of type you may call
Taxi toll(), getPlate() and company()
Car or Vehicle toll() and getPlate()
Commercial company()
Object only Object's methods, such as toString() and equals(...)

Object's methods can be called through all of them, since every object has those.

Up the tree, never across

A variable can refer to objects of its own type and of every type below it in the tree. Other assignments don't compile:

Main.java
public class Main {
    public static void main(String[] args) {
        Car car = new Truck("TR-88", "Nordic Freight", 3);
        Commercial customer = new Car("CR-55");
    }
}
Vehicle.java
public class Vehicle {
    private String plate;

    public Vehicle(String plate) {
        this.plate = plate;
    }
}
Car.java
public class Car extends Vehicle {
    public Car(String plate) {
        super(plate);
    }
}
Truck.java
public class Truck extends Vehicle implements Commercial {
    private String company;
    private int axles;

    public Truck(String plate, String company, int axles) {
        super(plate);
        this.company = company;
        this.axles = axles;
    }

    @Override
    public String company() {
        return this.company;
    }
}
Commercial.java
public interface Commercial {
    String company();
}
What javac prints
Main.java:3: error: incompatible types: Truck cannot be converted to Car
        Car car = new Truck("TR-88", "Nordic Freight", 3);
                  ^
Main.java:4: error: incompatible types: Car cannot be converted to Commercial
        Commercial customer = new Car("CR-55");
                              ^
2 errors
  • A Truck is a Vehicle, and so is a Car. But a Truck isn't a Car: classes side by side in the tree share only what's above them.
  • A plain Car isn't Commercial, even though Taxi, a class below it, is. Types are passed down the tree, never up: a Car doesn't get what its subclasses add.

Java's own classes

Java's own classes are built the same way. An ArrayList is a List and a Collection (module 35), and an Object:

ArrayList<String> plates = new ArrayList<>();
plates.add("TX-12");
plates.add("TR-88");
List<String> list = plates;
Collection<String> collection = plates;
Object thing = plates;

list.add("CR-55");
System.out.println(collection.size());
System.out.println(thing);
System.out.println(thing == collection);
Output
3
[TX-12, TR-88, CR-55]
true

Four variables, one list: list.add adds to the same list that collection.size() counts, and printing it through thing runs ArrayList's own toString. An ArrayList has a few more types, such as the abstract class AbstractList that module 35 mentioned. Java's documentation lists the superclasses and interfaces of each of its classes.

Your task

Challenge 1 of 3

At the end of the day, the booth adds up the tolls of every vehicle that went through, and lists the companies to bill. Fill in the type of each list. passed holds all three vehicles, and its loop calls toll() on each. customers holds the taxi and the truck, and its loop calls company(). Each list's type must be one that every object on it has, and that has the method its loop calls. The program prints:

Tolls today: 25
Bill to Metro Cabs
Bill to Nordic Freight

Challenge 2

The booth's truck, seen through several variables. What does this program print?

Challenge 3

Some trucks carry fuel, and pay 10 more than other trucks with the same number of axles. In TankerTruck.java, write the class TankerTruck, which extends Truck, with:

  • a constructor TankerTruck(String plate, String company, int axles)
  • toll() overridden to return Truck's toll plus 10: get Truck's toll with super.toll()

Don't write implements Commercial or company(): a TankerTruck gets both from Truck. The check runs:

TankerTruck tanker = new TankerTruck("TK-4", "Coastal Fuels", 3);
Vehicle vehicle = tanker;
Commercial customer = tanker;
Truck truck = tanker;
System.out.println(vehicle);
System.out.println(customer.company() + " " + truck.toll());

and expects:

TK-4 pays 25
Coastal Fuels 25