Skip to main content

CSE 331: Subtyping

lecture

Course motto: Your job is to deliver code that you know works.

Every argument this course has made about a method call used the specification of the method called, and never its code. Lecture 2 made that the point of a specification: the client reads the promise and reasons from it, and the implementation can change underneath. Java adds a twist. A method written to take one type can be handed an object of another type, as long as the compiler agrees, and the object it is handed may belong to a class that did not exist when the method was written. This lecture asks what that does to the argument. When a client was reasoned about against one type's specification, is the reasoning still right once an object of a subtype arrives? The answer is a definition, the behavioral subtype, and a rule that the compiler cannot keep for you.

A SaleProduct where a Product is expected

Here is a method that adds up the prices in a shopping cart. Its specification is one line of Javadoc, and its loop asks each product in the cart for its price:

Products.java
25
26
27
28
29
30
31
32
/** @return the sum of the prices of the products in cart */
public static double total(List<Product> cart) {
    double sum = 0.0;
    for (Product p : cart) {
        sum = sum + p.getPrice();
    }
    return sum;
}

total is written against one class, Product. Its parameter is a list of Products, and the only method of Product it calls is getPrice, in its loop.

Here is a main that fills a cart with two products and hands the cart to total:

Products.java
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
    List<Product> cart = new ArrayList<>();
    cart.add(new Product("skittles", 2.0));
    cart.add(new SaleProduct("m&ms", 2.0, 0.5));
    for (Product p : cart) {
        System.out.println(p);
    }
    System.out.println("total: $" + total(cart));
}

println prints an object by calling the object's toString method,3 so main 's loop prints each product as its toString says. Here are the two classes, Product and SaleProduct:

Products.java
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/** A product for sale, with a name and a price in dollars. */
class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    /** @return the name of this product */
    public String getName() {
        return name;
    }

    /** @return the price of this product, in dollars */
    public double getPrice() {
        return price;
    }

    /** @return this product's name and price, as "name: $price" */
    @Override
    public String toString() {
        return name + ": $" + price;
    }
}

/** A product on sale: a fraction of its price is taken off. */
class SaleProduct extends Product {
    private double discount;  // the fraction taken off, 0 to 1

    public SaleProduct(String name, double price, double discount) {
        super(name, price);
        this.discount = discount;
    }

    /**
     * @return the price of this product with its discount taken
     *     off
     */
    @Override
    public double getPrice() {
        return super.getPrice() * (1 - discount);
    }
}

SaleProduct is declared extends Product. It adds one field, discount, the fraction of the price taken off. Its constructor passes the name and the price on to Product's, with super(name, price). Its getPrice asks Product's getPrice for the price, with super.getPrice(), and takes the discount off.

When a class writes a method with the same name and parameters as a method of the class it extends, the class is said to override that method. A call of the method on an object of the class then runs the class's own body, even in code written against the class it extends. So a call of getPrice on a SaleProduct runs SaleProduct's body, not Product's. The @Override above SaleProduct's getPrice asks the compiler to check that Product has a getPrice to override. Product's toString likewise overrides the one every Java class starts with, from Object.

Now look at the call of add that puts the m&ms in the cart. cart is a List<Product>, so its add method takes a Product. That call hands add a SaleProduct instead. Does the compiler accept that call? If it does, what do the two calls of println print? Write down your prediction before you read the run.

$ java Products
skittles: $2.0
m&ms: $2.0
total: $3.0

The compiler accepted that call. main ran to the end. The last line is the one total computed, so start there. The skittles cost 2 dollars. The m&ms cost 2 dollars with half taken off, which is 1 dollar. So $3.0 is what the two cost together. To get it, total's loop called getPrice on the SaleProduct, and the call ran SaleProduct's getPrice, though nothing in total mentions SaleProduct.

The second line is another matter. The m&ms are half off, and the line shows their full price, $2.0! Add up the first two lines, and you get 4 dollars, not the $3.0 under them. Keep that line in mind.

A wider rule. Many of us learned a simple rule for Java's types: a parameter of type Product must be passed a Product. That rule is why javac refuses a call that passes 17 to a parameter of type String. The call that puts the m&ms in the cart breaks the simple rule, yet javac accepted that call. There, the compiler checked something wider: that SaleProduct reaches Product through the extends in SaleProduct's declaration.

Definition: Java subtype

A type B is a Java subtype of a type A if the compiler accepts a B where an A is expected. The compiler accepts it when B reaches A by a chain of extends and implements: B extends or implements A, or B extends or implements a type that reaches A. A is then a Java supertype of B.

Here is what three of the box's words mean:

  • Expected means the compiler knows the type a place needs: the argument for a parameter of type A, a value stored in a variable declared with type A, or a value returned from a method declared to return an A. In the call that puts the m&ms in the cart, the place was add's parameter.
  • Chain means a Java subtype may be more than one step away. SaleProduct reaches Product in one step, by its extends. Java's ArithmeticException reaches Exception in two: ArithmeticException extends RuntimeException, which extends Exception.
  • Java is in the name because the compiler decides the question from the extends and implements declarations alone, and never from a specification.1

Lecture 10 made the same point about implements: "The compiler cannot check whether each body does what its specification says."

You have been handing Java subtypes to code written for their supertypes for weeks, code that never mentions the subtype:

  • An ArrayList in a List variable. main above stores an ArrayList in a variable declared as a List<Product>, and ArrayList implements List. Lecture 1's Sum did the same with its variable list, and so did Lecture 10's ListQueue.
  • Lecture 10's interfaces and test subclasses. ListQueue implements HelpQueue, so ListQueueSpecTest's emptyQueue, declared to return a HelpQueue, could return a ListQueue. And ListQueueSpecTest extends HelpQueueTest, so JUnit could run the five tests HelpQueueTest declares on a ListQueueSpecTest.
  • Lecture 13's exception classes. A catch for a class, that lecture said, takes an exception "of that class or of any class that extends it". The calculator's catch for CalcException took a DivideByZeroException, one step down. TraceSwallow's catch for Exception took an ArithmeticException, two steps down.

The lecture's question. total is one more such case. Here is the argument that total satisfies its specification, made from specifications alone, as Lecture 2 reasoned across a call. sum starts at 0.0. Each time around the loop, total adds p.getPrice() to sum. Product's specification of getPrice promises the price of this product, in dollars. So after the loop, sum is the sum of the prices of the products in cart. That is the promise in total's Javadoc.2

That argument used one fact about the products in cart: Product's promise about getPrice. The argument used nothing from Product's body. total never mentions SaleProduct, so the argument could not have used SaleProduct's body either. SaleProduct could have been written a year after total, by someone else. Yet main handed total a SaleProduct, and total's call of getPrice ran SaleProduct's getPrice. Its specification promises something Product's specification does not: the price of this product with its discount taken off.

total was argued correct against Product's specification. So the question for the rest of this lecture is this: when a SaleProduct arrives, or any Java subtype of Product written later, is that argument still right? And what would a Java subtype have to promise to make it right?

Before we can answer, we need to know what extends Product gave SaleProduct besides the compiler's permission to put a SaleProduct in cart. The m&ms line, at full price, is where to start.

Subclassing is copying code

The m&ms are half off, so they should print at $1.0. Yet the run printed m&ms: $2.0. In the same run, total counted the m&ms at $1.0, since the total came to $3.0 and not $4.0. So one object reported two different prices in one run. To see how, we need to know what extends Product did for SaleProduct. Here is SaleProduct again:

Products.java
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/** A product on sale: a fraction of its price is taken off. */
class SaleProduct extends Product {
    private double discount;  // the fraction taken off, 0 to 1

    public SaleProduct(String name, double price, double discount) {
        super(name, price);
        this.discount = discount;
    }

    /**
     * @return the price of this product with its discount taken
     *     off
     */
    @Override
    public double getPrice() {
        return super.getPrice() * (1 - discount);
    }
}

SaleProduct's own lines declare just three things: the field discount, a constructor, and the method getPrice. Yet they declare no name field and no toString method, and the run still printed the m&ms by name, through a toString. Those come from Product. extends Product makes SaleProduct behave as if Product's fields and methods had been copied into it. So every SaleProduct object has the fields name and price.4 It also has the methods getName and toString, exactly as Product wrote them. The constructor's first line, super(name, price), runs Product's constructor. Product's constructor sets the copied fields.

SaleProduct also declares getPrice, a method Product already has, so SaleProduct's getPrice overrides Product's, as the last section said. For a SaleProduct object, SaleProduct's getPrice runs in place of Product's, and super.getPrice() reaches the one it replaced, to get the full price before the discount comes off.

This copying is called inheritance. A class gets the fields and the methods of another class without writing them out, except the methods it overrides. What is the alternative? Without extends, we could write SaleProduct out in full ourselves. We would copy Product's class, rename it, add the discount, and change getPrice. That would work. The program would print the same three lines. Now suppose the store decides to print prices with two decimal places, $1.00 instead of $1.0. That change belongs in toString, and there are now two copies of toString to change. With a third kind of product copied the same way, there would be three. A copy that missed the change would print its prices the old way. Nothing would warn us. With inheritance there is one toString, in Product. A change there reaches SaleProduct, and every other subclass of Product, with no copy to forget.

Copying explains the bug too. main's loop prints each product through its toString. SaleProduct does not override toString, so for the m&ms the toString that runs is Product's, the one SaleProduct inherited:

Products.java
55
56
57
58
59
/** @return this product's name and price, as "name: $price" */
@Override
public String toString() {
    return name + ": $" + price;
}

Meanwhile total asks each product in the cart for its price:

Products.java
25
26
27
28
29
30
31
32
/** @return the sum of the prices of the products in cart */
public static double total(List<Product> cart) {
    double sum = 0.0;
    for (Product p : cart) {
        sum = sum + p.getPrice();
    }
    return sum;
}

Each listing has one line that gets a price for the m&ms. Before you read on, find the line that gives the m&ms their full price, and say why the other line does not.

Product's toString reads the field price itself. For the m&ms, that field holds 2.0, the price main gave the m&ms, which Product's constructor stored. Overriding getPrice changed which getPrice runs on a SaleProduct. It did not change the field, and toString never calls getPrice. Picture the copy of toString inside SaleProduct: it still reads the field, and the field still holds the full price.

total, on the other hand, calls getPrice. The variable p is declared a Product, but Java chooses which getPrice runs by the class of the object p refers to, not by the type p was declared with. This choice, made each time the call runs, is called dynamic dispatch. On the m&ms, p refers to a SaleProduct, so the call in total runs SaleProduct's getPrice and gets 1.0. So one object reported two prices because total called the overridden getPrice and toString read the field.

When toString was written, reading price and calling getPrice gave the same answer, since Product's getPrice just returns price. The two stopped agreeing only once a subclass overrode getPrice. ProductsFixed.java is Products.java changed in toString alone, to call getPrice. Its opening comment is four lines shorter, so the changed line is line 54 there:

ProductsFixed.java
51
52
53
54
55
/** @return this product's name and price, as "name: $price" */
@Override
public String toString() {
    return name + ": $" + getPrice();
}

Now toString calls getPrice, and on the m&ms dynamic dispatch runs SaleProduct's getPrice, as it does in total:

$ java ProductsFixed
skittles: $2.0
m&ms: $1.0
total: $3.0

The m&ms now print at $1.0. The total is still $3.0.

Four words. Writing extends Product did two things at once. SaleProduct got Product's code. And a SaleProduct became something Java lets us put in cart, a List<Product>, and hand to total. These are different ideas, and Java joins them in one keyword. The rest of the lecture keeps them apart with four words: one for each of the two ideas, one for the keyword that joins them, and one for a third idea, which the next section defines:

  • Inheritance: a class gets the fields and the methods of another class without writing them out, except the methods it overrides.
  • Java subclassing: writing class B extends A, which in Java grants inheritance and Java subtyping together.
  • Java subtyping: B is a Java subtype of A if the compiler accepts a B where an A is expected, which it does when B reaches A by a chain of extends and implements.
  • Behavioral subtyping: B is a behavioral subtype of A when a B keeps every promise that A's specification makes, an idea the next section makes exact.

The first three words are about code: what gets copied, and what the compiler accepts. You write Java subclassing, as extends, and it brings the other two with it. Java subtyping can come without inheritance. ListQueue implements HelpQueue in the lecture on ADTs, so a ListQueue may go where a HelpQueue is expected. Nothing is copied, since HelpQueue has no fields and no method bodies. The fourth word is about specifications. How much of a specification the compiler checks is a question for later in this lecture.

The fixed program printed the right price on every line. But look at what total used to get its answer right, in both programs. total was written against Product. Its loop calls getPrice, whose Javadoc in Product promises the price of this product, in dollars. Nothing in total mentions SaleProduct, and total could have been written, and reasoned about, before SaleProduct existed. Our reasoning about total never looked at SaleProduct's code at all. So when a SaleProduct arrives at total's call of getPrice, what was that reasoning relying on?

A behavioral subtype keeps every promise

total's argument used one promise about products: Product's specification of getPrice, that it returns the price of the product, in dollars. The argument never looked at SaleProduct. It could not have, since a class like SaleProduct can be written a year after total, by someone who never read total, and Java will still let it into the cart.

Lecture 2 set the standard such an argument has to meet. A client is correct, it said, "if it works with every implementation that meets the specifications it relies on". Call the type a client was argued against A, and the type that arrives later B. An object of B is one more implementation of A's methods. So every argument made against A's specification is still right for a B exactly when B's specification implies A's: when every object that satisfies B's specification also satisfies A's.

Lecture 2 made that comparison between two specifications of a method, with meets for our satisfies, and called the first one stronger:

A specification S1 is stronger than a specification S2 if every implementation that meets S1 also meets S2.

Here is the same comparison made for a whole type at once.

Definition: Behavioral subtype

A type B is a behavioral subtype of a type A if every object that satisfies B's specification also satisfies A's.

Method by method, each method B shares with A has a specification in B at least as strong as its specification in A: a precondition no stronger, and a postcondition no weaker.

A type here is a class or an interface, and an object satisfies a type's specification when each of its methods satisfies that method's specification in the type. Method by method applies Lecture 2's rule: S1 is stronger than S2 "when S1's precondition is weaker than or the same as S2's, and, on every call where S2's precondition is true, S1's postcondition is stronger than or the same as S2's." So no stronger means weaker or the same, and no weaker is read on the calls A's precondition allows. B may also add methods, which a client argued against A never calls.

IndexOf.java puts the definition to work on two pairs of interfaces. Each declares one method, indexOf, which searches an array a for a value x:

IndexOf.java
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/** Finds a value known to be in an array. */
interface Any {
    /**
     * @requires x occurs in a
     * @return an index i such that a[i] == x
     */
    int indexOf(int[] a, int x);
}

/** Finds the first occurrence of a value known to be in an array. */
interface First extends Any {
    /**
     * @requires x occurs in a
     * @return the smallest index i such that a[i] == x
     */
    int indexOf(int[] a, int x);
}

/** Finds a value in an array, if it is there. */
interface Finder {
    /**
     * @return the smallest index i such that a[i] == x, or -1 if x
     *     does not occur in a
     */
    int indexOf(int[] a, int x);
}

/** Finds a value known to be in an array. */
interface PresentFinder extends Finder {
    /**
     * @requires x occurs in a
     * @return the smallest index i such that a[i] == x
     */
    int indexOf(int[] a, int x);
}

Java accepts both pairs: IndexOf.java compiles. But one pair is a behavioral subtype and the other is not. Compare each pair's preconditions, then its postconditions, and decide which is which before you read on.

First is a behavioral subtype of Any. The two preconditions are the same, x occurs in a. So we need to compare only the postconditions, as Lecture 2 did for its specifications B and D. The smallest index i such that a[i] == x is in particular an index i such that a[i] == x. So First's postcondition is stronger than Any's, and every object that satisfies First satisfies Any.

PresentFinder is not a behavioral subtype of Finder. This time the postconditions agree wherever both apply: on a call where x occurs, each promises the smallest index. The preconditions differ. Finder has none, so a call where x does not occur meets it, and on that call Finder promises -1. PresentFinder requires x to occur in a, so the same call breaks its precondition, and PresentFinder promises nothing. Its precondition is stronger, so its specification is weaker, as Lecture 2 found its specification A weaker than D. An object that satisfies PresentFinder may do anything at all when x is absent, so it need not satisfy Finder.

Does the difference matter, when Java accepted both? Here is a client written against Finder:

IndexOf.java
16
17
18
public static boolean contains(Finder finder, int[] a, int x) {
    return finder.indexOf(a, x) != -1;
}

Let's argue that contains returns whether x occurs in a, using Finder's specification alone. Finder's indexOf has no precondition, so the call in contains meets it, whatever a and x are. The postcondition says the call returns the smallest index where x occurs, or -1 if x does not occur. No index of an array is -1. So the call returns -1 exactly when x does not occur in a, and contains returns true exactly when x occurs. The argument used nothing but Finder's specification, so it is right for every object that satisfies Finder, including objects of classes not yet written.

main hands contains two finders:

IndexOf.java
20
21
22
23
24
25
26
27
28
public static void main(String[] args) {
    int[] a = {5, 3, 8, 3};
    Finder scan = new Scan();
    System.out.println("3 in a: " + contains(scan, a, 3));
    System.out.println("7 in a: " + contains(scan, a, 7));
    Finder assuming = new ScanAssuming();
    System.out.println("3 in a: " + contains(assuming, a, 3));
    System.out.println("7 in a: " + contains(assuming, a, 7));
}

Scan implements Finder: it returns the first match, or -1 once its loop has passed the end. ScanAssuming implements PresentFinder:

IndexOf.java
79
80
81
82
83
84
85
86
87
88
/** A PresentFinder that scans until it finds the value. */
class ScanAssuming implements PresentFinder {
    public int indexOf(int[] a, int x) {
        int i = 0;
        while (a[i] != x) {
            i = i + 1;
        }
        return i;
    }
}

ScanAssuming is Lecture 2's Implementation 3, with the whole array as its range: it stops at the first match and never checks for the end of the array. It satisfies PresentFinder's specification, since on every call that meets the precondition there is a match to stop at. The declaration of assuming in main compiles, because a ScanAssuming is a PresentFinder, and to Java a PresentFinder is a Finder. Predict what main prints, then read the run:

$ java IndexOf
3 in a: true
7 in a: false
3 in a: true
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
    at ScanAssuming.indexOf(IndexOf.java:83)
    at IndexOf.contains(IndexOf.java:17)
    at IndexOf.main(IndexOf.java:27)

Scan gives the right answer both times, and so does ScanAssuming for 3. For 7, ScanAssuming runs off the end: its test a[i] != x reads a[4] in an array of length 4. The call in contains ran the indexOf of ScanAssuming, the class of the object finder refers to, though finder is declared a Finder.

So which code is wrong? Read each piece against its own specification. ScanAssuming satisfies PresentFinder's: on the last call, 7 does not occur in a, so the call broke the precondition, and PresentFinder promises nothing. contains is correct against Finder's specification, by the argument above. The mistake is the extends in PresentFinder's declaration. It declares that a PresentFinder may go wherever a Finder is expected, and PresentFinder's specification does not keep Finder's promise for a value that is absent.

Said from the side of a client, which was argued against A and is handed a B, the definition is the Liskov substitution principle: if B is a behavioral subtype of A, then a B may be used wherever an A is expected, and every argument made against A's specification is still right for the B. The principle is named for Barbara Liskov, who stated the idea in a talk in 1987.5

Java accepted both pairs: the extends in First's declaration, which keeps Any's promises, and the one in PresentFinder's, which does not keep Finder's. Which of these promises does the compiler check at all?

What the compiler checks

To see what the compiler checks, we need a pair of interfaces it can refuse. In both IndexOf pairs, the two declarations of indexOf have the same types, so neither pair gives javac anything to refuse. Checks.java has a pair with more in its types. A Source is a sequence of numbers kept somewhere, perhaps in a file, and its method positions finds a value among them. An ArraySource keeps its numbers in an array in memory. Before reading on, look at the two declarations of positions and ask which parts of their specifications javac could compare.

Checks.java
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/** A sequence of numbers kept somewhere, such as in a file. */
interface Source {
    /**
     * @return a list of the indexes at which x occurs in this
     *     sequence, in increasing order
     * @throws IOException if the numbers could not be read
     */
    List<Integer> positions(int x) throws IOException;
}

/** A sequence of numbers kept in an array in memory. */
interface ArraySource extends Source {
    /**
     * @return a new ArrayList of the indexes at which x occurs in
     *     this sequence, in increasing order
     */
    ArrayList<Integer> positions(int x);
}

Reading a file can fail, so Source's positions declares IOException, the checked exception that Lecture 9's server declared for the network. An ArraySource's numbers are already in memory, so its positions cannot fail to read them. ArraySource's positions declares no exception. It also returns an ArrayList where Source's returns a List. The two declarations have the same name and parameter types, so ArraySource's positions overrides Source's, as SaleProduct's getPrice overrides Product's.

Now compare the two specifications by Lecture 2's rule. The two have the same precondition, none. Every ArrayList is a List, so a client promised an ArrayList has been promised a List and more. Source promises an IOException when the numbers could not be read, and an ArraySource's numbers can always be read, so that case never arises. So ArraySource's specification is the stronger, and ArraySource is a behavioral subtype of Source. javac Checks.java compiles the file and prints nothing.

The return type. Now change one line. Here is a copy of Checks.java in which ArraySource's positions returns a java.util.Collection<Integer> instead. Collection is a Java supertype of List, so every list is a Collection, but a Collection need not be a list. (The java.util. in front saves an import.) Should javac accept the copy?

$ javac Checks.java
Checks.java:36: error: positions(int) in ArraySource clashes with positions(int) in Source
    java.util.Collection<Integer> positions(int x);
                                  ^
  return type Collection<Integer> is not compatible with List<Integer>
1 error

javac refuses it. A client of Source may call get(0) on what positions returns, and only a list is sure to have a get. So an override may return the same type as the method it overrides, or a subtype of that type, as ArraySource's positions in Checks.java returns an ArrayList. It may never return a supertype.

The checked exceptions. Next, a copy in which ArraySource's positions declares throws Exception. Should javac accept this one?

$ javac Checks.java
Checks.java:36: error: positions(int) in ArraySource clashes with positions(int) in Source
    ArrayList<Integer> positions(int x) throws Exception;
                       ^
  overridden method does not throw Exception
1 error

javac refuses this one too. A client of Source has to catch IOException or declare it, by Lecture 6's rule. Say it catches. A catch for a class takes an exception of that class or of any class that extends it, as Lecture 13 showed. So an Exception that is not an IOException would pass that client's catch by. Each checked exception an override declares must therefore be one that the overridden method declares, or a subclass of one. An override may declare fewer checked exceptions, as ArraySource's positions declares none, or narrower ones. It may never declare a new one.6

Both checks are Lecture 2's comparison, made on the part of a postcondition that the types carry. Lecture 2 put the postcondition "at the point where it returns or throws", so what a method returns and which exceptions it throws belong to the postcondition. A return type and a throws clause write some of that in code, where javac reads it. javac checks that the override's part is the same or stronger. An unchecked exception appears in no throws clause, so javac checks nothing about one. ScanAssuming threw an ArrayIndexOutOfBoundsException that Finder never mentions, and IndexOf.java compiled.

What the compiler cannot check. The rest of a specification is in the Javadoc. javac checks no @requires, @return, or @effects clause against anything. Look again at Finder and PresentFinder in IndexOf.java. Finder's declaration of indexOf and PresentFinder's are the same line. What makes PresentFinder's specification weaker, its @requires clause, is in a comment. So javac sees two identical declarations and accepts the extends in PresentFinder's declaration. Swap the two Javadoc comments, so that Finder asks for x to occur and PresentFinder promises -1, and javac accepts that program too. Swapped, PresentFinder would be a behavioral subtype of Finder. So javac accepts the extends whether or not PresentFinder keeps Finder's promises.

A wider parameter. Of the precondition, Lecture 2's rule asks only that "S1's precondition is weaker than or the same as S2's". So a subtype may also strengthen a specification by weakening its precondition. In the types, a weaker precondition is a wider parameter type, one that accepts more values. A long holds every int and more, as Lecture 1's exercise 6 showed. So here is a copy in which ArraySource's positions takes long x. The copy also adds @Override above that declaration, to ask javac to check that the method overrides one:

$ javac Checks.java
Checks.java:36: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

The method overrides nothing. Java tells two methods apart by their names and their parameter types, so positions(long) is a second method named positions, beside the positions(int) that ArraySource gets from Source. Two methods with the same name and different parameter types are overloads of the name. Without the @Override line, javac accepts the copy. ArraySource then has two methods named positions. A client of Source still calls positions(int), which promises what it always did. So Java gives an override no way to accept a wider parameter type. A subtype can weaken a precondition only in its Javadoc, where javac does not look.

So javac checks an override's return type and its checked exceptions, in the direction Lecture 2's rule asks, and no @requires or @effects clause. The rest of the comparison falls to the programmer who writes extends.

Rule: declare only behavioral subtypes

Write B extends A or B implements A only if B is a behavioral subtype of A.

javac checks the return types and the checked exceptions of B's overrides against A's methods, and nothing in A's Javadoc. A B that breaks one of A's other promises compiles, and every client reasoned about against A goes on believing that promise about the B it is handed.

The reason is the client's argument. contains was argued against Finder's specification alone, and that argument is still right for every behavioral subtype of Finder, including one written after contains. PresentFinder is not a behavioral subtype of Finder, so the argument for contains has to be made again for a PresentFinder. So does the argument for every other client of Finder. For a type with many clients, as Lecture 2 said of a library, no one can check them all.

The rule sounds easy to keep: compare the specifications, then write extends. The next section is a case where the is-a test (is every B an A?) says yes and the specifications say no.

Is a Square a Rectangle?

To keep the rule, we have to decide whether a class is a behavioral subtype before we write extends. The test most programmers reach for first is the is-a test: B should extend A when every B is an A. A square is a rectangle whose width and height happen to be equal, so every square is a rectangle. So let's write it that way. Shapes.java has a class Rectangle with a width and a height, a method to get each, and a method to set each, setWidth and setHeight. Here is its Square:

Shapes.java
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/** A rectangle whose width and height are always equal. */
class Square extends Rectangle {
    public Square(int side) {
        super(side, side);
    }

    /**
     * @modifies this
     * @effects sets this square's width and height to w
     */
    @Override
    public void setWidth(int w) {
        super.setWidth(w);
        super.setHeight(w);
    }

Square declares its own setWidth, which Java runs in place of Rectangle's on any Square, even through a variable declared as a Rectangle. (Java says Square overrides setWidth.) In it, super.setWidth(w) calls Rectangle's setWidth on this square. So a square's setWidth sets both its width and its height to w, and Square's setHeight does the same. javac compiles all of it, so Square is a Java subtype of Rectangle.

Now for a client. widen takes any Rectangle and changes its width:

Shapes.java
14
15
16
17
18
19
20
21
22
23
/**
 * @modifies r
 * @effects sets r's width to w; r's height is unchanged
 * @return the area of r afterward
 */
public static int widen(Rectangle r, int w) {
    int h = r.getHeight();
    r.setWidth(w);
    return w * h;
}

widen was written, and reasoned about, against Rectangle's specification of setWidth:

Shapes.java
57
58
59
60
61
62
63
64
/**
 * @modifies this
 * @effects sets this rectangle's width to w; its height is
 *     unchanged
 */
public void setWidth(int w) {
    width = w;
}

widen first saves r 's height in h. Next it calls setWidth, whose specification promises that afterward r 's width is w and its height is still h. So the area of r is w * h, which widen returns. This argument used only Rectangle's specification, so it is right for every object that satisfies that specification. main widens a 2 by 3 rectangle and a 2 by 2 square to a width of 5, and prints what widen returned beside the area each shape then has. Predict the two lines, then compare:

$ java Shapes
rectangle: widen returned 15, area is 15
square: widen returned 10, area is 25

The rectangle line agrees: the rectangle is now 5 by 3, with an area of 15. The square line does not. widen returned 10, five times the old height of 2. But the square is now 5 by 5, with an area of 25, so widen's @return is false on this run.

Before reading on, decide which method is to blame: widen, Rectangle's setWidth, or Square's setWidth?

Rectangle's setWidth satisfies its specification, and Square's satisfies its own. widen's argument is right for any object that satisfies Rectangle's specification. So the trouble is in how the two specifications of setWidth compare. Lecture 2 defined the comparison: a specification S1 is stronger than S2 "if every implementation that meets S1 also meets S2", and two that are neither stronger nor weaker are incomparable. Neither specification of setWidth has a precondition, so we compare their postconditions on the run's call, setWidth(5) on a 2 by 2 square:

  • Square's is not stronger. Square's setWidth satisfies its own specification and leaves the height 5, where Rectangle's postcondition says 2.
  • Square's is not weaker. Rectangle's setWidth satisfies its own specification and leaves the height 2, where Square's postcondition says 5.

The two specifications are incomparable. A behavioral subtype needs, for each method it shares with its supertype, a specification at least as strong. setWidth fails that, and setHeight fails it the same way. So Square is a Java subtype of Rectangle that is not a behavioral subtype, and the extends in Square's declaration breaks the rule. The methods to blame are the two that set, and the fault is in their specifications, not their code.

No better setWidth could fix Square. A square promises that its width and height are equal, and Rectangle's setWidth promises a new width with the old height. On a square, with w different from the side, no call can keep both promises. A square that never changed would break neither.

Way out 1: shapes that cannot change. A class is immutable if no object of the class can change once its constructor has finished. ImmutableShapes.java has a Rectangle like that. Its class comment says A rectangle cannot be changed once it is made. In place of setWidth, it has a method that makes a new rectangle:

ImmutableShapes.java
49
50
51
52
53
54
55
/**
 * @return a new rectangle whose width is w and whose height is
 *     this rectangle's height
 */
public Rectangle withWidth(int w) {
    return new Rectangle(w, height);
}

withHeight is the same with the width and the height trading places. Square extends this Rectangle and declares nothing but a constructor, super(side, side). Does a Square keep withWidth 's promise? main calls withWidth(5) on a 2 by 3 rectangle and on a 2 by 2 square. Work out the square's line before you look:

$ java ImmutableShapes
rectangle: 2 by 3, withWidth(5) returned 5 by 3
square: 2 by 2, withWidth(5) returned 5 by 2

The square kept the promise: it is unchanged, and the rectangle it returned has width 5 and the square's height. Square inherits every method of Rectangle, so each of Square's methods has Rectangle's specification, word for word. A specification is as strong as itself, so Square is now a behavioral subtype of Rectangle. (A square's withWidth returns a Rectangle. One that returned a 5 by 5 Square would break the promise.)

Way out 2: hold a rectangle instead. Sometimes shapes must change in place. Then Square can use Rectangle's code without being a Java subtype of it. In ComposedSquare.java, Rectangle and widen are Shapes.java's, unchanged, and Square holds a Rectangle in a field instead of extending it:

ComposedSquare.java
77
78
79
80
81
82
83
84
85
86
87
88
89
/** A square: a width and a height that are always equal. */
class Square {
    // RI: rect's width equals rect's height
    private Rectangle rect;

    public Square(int side) {
        rect = new Rectangle(side, side);
    }

    /** @return the width of this square */
    public int getWidth() {
        return rect.getWidth();
    }

Building a class from an object of another class, held in a field, is called composition. Each method of Square does its work by calling methods of rect. A method that passes its work on to an object held in a field this way is forwarding the call. getHeight forwards the same way. setSide calls rect.setWidth(s) and then rect.setHeight(s), so it keeps the representation invariant stated above rect. Square reuses Rectangle's code without copying it, and it offers no setWidth. Nor is a Square a Java subtype of Rectangle any more. A copy of ComposedSquare.java whose main passes its square to widen, as Shapes.java's did, does not compile: javac reports Square cannot be converted to Rectangle.

The JDK has a class built the way Shapes.java built Square. java.util.Properties, a table of string keys and string values, extends java.util.Hashtable, a table of any objects. Its documentation warns that Hashtable's put can store a key that is not a string, after which Properties's store fails. It says of put and putAll: "Their use is strongly discouraged."7 A Properties that held its Hashtable in a field would have kept put out of reach.

Every square is a rectangle, but whether Square may extend Rectangle depends on what Rectangle promises. The lecture asked whether a client's reasoning against A is still right once a B arrives. widen's argument used only Rectangle's promises, and a Square that broke one of them made its conclusion false. The reasoning a client did against the supertype stays right exactly when the subtype keeps every promise. So we never declare a Java subtype that is not a behavioral subtype.


  1. The Java Language Specification, Java SE 25 edition, section 4.10, gives the full definition. It covers primitive types and arrays too, and counts every type a Java subtype of itself. ↩

  2. Up to rounding, that is: each + in the loop rounds to a double, and this lecture leaves rounding aside. ↩

  3. The documentation of PrintStream.println(Object) in Java 25 says that it calls String.valueOf on the object, and String.valueOf returns the object's toString, or "null" for a null reference. ↩

  4. The two fields are private, so only Product's own lines can name them. SaleProduct's lines reach the price through super.getPrice(). The Java Language Specification (Java SE 25) says private members are not inherited, in §8.2, and also that a new object has room for "all the instance variables declared in each superclass of the class", in §12.5. So in the specification's words SaleProduct does not inherit the two fields, yet every SaleProduct carries both. ↩

  5. Barbara Liskov, "Data Abstraction and Hierarchy", her keynote at the OOPSLA conference, 1987. Barbara Liskov and Jeannette Wing, "A Behavioral Notion of Subtyping", ACM Transactions on Programming Languages and Systems, 1994, define a behavioral subtype by its specifications, with a rule for each method like the box's. ↩

  6. The Java Language Specification, Java SE 25 edition, states both requirements in section 8.4.8.3, and section 9.4.1.2 applies them to an interface's methods. For a primitive return type such as int, section 8.4.5 asks for the identical type. Section 9.4.2 calls a name shared by two methods with different parameter types overloaded. ↩

  7. Oracle, Java SE 25 API Specification (2025), the documentation of the class java.util.Properties, at https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Properties.html. ↩