Lecture 5: Methods for self-referential lists
Designing classes to represent lists. Methods on lists, including basic recursive methods, and sorting
Lecture outline
Representing lists
Basic methods
Sorting
5.1 Representing lists
The following class diagram defines a class hierarchy that represents a list of books in a bookstore:
+--------------------------+ | ILoBook |<----------------+ +--------------------------+ | +--------------------------+ | | int count() | | | double totalPrice() | | | ILoBook allBefore(int y) | | | ILoBook sortByPrice() | | +--------------------------+ | | | / \ | --- | | | ----------------------------- | | | | +--------------------------+ +--------------------------+ | | MtLoBook | | ConsLoBook | | +--------------------------+ +--------------------------+ | +--------------------------+ +-| Book first | | | int count() | | | ILoBook rest |-+ | double totalPrice() | | +--------------------------+ | ILoBook allBefore(int y) | | | int count() | | ILoBook sortByPrice() | | | double totalPrice() | +--------------------------+ | | ILoBook allBefore(int y) | | | ILoBook sortByPrice() | | +--------------------------+ v +---------------+ | Book | +---------------+ | String title | | String author | | int year | | double price | +---------------+
Let’s make some examples
//Books Book htdp = new Book("HtDP", "MF", 2001, 60); Book lpp = new Book("LPP", "STX", 1942, 25); Book ll = new Book("LL", "FF", 1986, 10); // lists of Books ILoBook mtlist = new MtLoBook(); ILoBook lista = new ConsLoBook(this.lpp, this.mtlist); ILoBook listb = new ConsLoBook(this.htdp, this.mtlist); ILoBook listc = new ConsLoBook(this.lpp, new ConsLoBook(this.ll, this.listb)); ILoBook listd = new ConsLoBook(this.ll, new ConsLoBook(this.lpp, new ConsLoBook(this.htdp, this.mtlist)));
5.2 Basic list computations
Given this preceding class diagram, we would like to design methods to answer the following questions
Count how many books we have in this list of books
Compute the total price of all books in this list of books
Given a date (year) and this list of books, produce a list of all books in this list that were published before the given year
Produce a list of the same books as this list, but sorted by their price
Each of the four questions concerns a list of books, and so we start by designing the appropriate method headers and purpose statements in the interface ILoBook:
// In ILoBook // ------- // count the books in this list int count(); // produce a list of all books published before the given date // from this list of books ILoBook allBefore(int year); // calculate the total price of all books in this list double totalPrice(); // produce a list of all books in this list, sorted by their price ILoBook sortByPrice();
We now have to define these methods in both the class MtLoBook and in the class ConsLoBook. (You may find it helpful to recall similar functions in DrRacket. Remember from last lecture the pattern given to us by virtue of Dynamic dispatch: we take each clause of the cond that checked for a particular variant and “move” the right-hand sides of those clauses into the methods defined in the corresponding class, then eliminate the cond altogether.)
The design recipe asks us to make examples. For clarity, we write them in an abbreviated manner, just showing the actual computation and the expected outcome:
// Examples for the class MtLoBook // ---------------------------- mtlist.count() => 0 mtlist.totalPrice() => 0 mtlist.allBefore() => mtlist
and our methods become:
// In MtLoBook: // --------- // count the books in this list public int count() { return 0; } // produce a list of all books published before the given date // from this empty list of books public ILoBook allBefore(int year) { return this; } // calculate the total price of all books in this list public double totalPrice() { return 0; }
Notice that the values produced by these methods are the base case values we have been in DrRacket for the empty lists. The count for an empty list is zero; the totalPrice of no Books is zero as well; and starting with an empty list there are no Books at all, let alone any before a given year.
Note: We will return to the sort method later.
Of course, there will be more work to do in the ConsLoBook class. First, examples!
// Examples // -------- lista.count() => 1 listc.count() => 3 lista.totalPrice() => 25 listd.totalPrice() => 95 lista.allBefore(2000) => lista listb.allBefore(2000) => mtlist listc.allBefore(2000) => new ConsLoBook(lpp, new ConsLoBook(ll,mtlist))
The design recipe asks us now to derive the template. A template serves as a starting point for any method inside ConsLoBook:
/* TEMPLATE: --------- Fields: ... this.first ... -- Book ... this.rest ... -- ILoBook Methods: ... this.count() ... -- int ... this.totalPrice() ... -- double ... this.allBefore(int year) ... -- ILoBook Methods for Fields: ... this.rest.count() ... -- int ... this.rest.totalPrice() ... -- double ... this.rest.allBefore(int year) ... -- ILoBook */
count and totalPrice
In the template, this.rest.count()
produces the count of all books in the rest of this list —
// count the books in this list int count() { return 1 + this.rest.count(); }
In the template, this.rest.totalPrice()
produces the total price of all books in the rest of this list —
// calculate the total price of all books in this list double totalPrice() { return this.first.price + this.rest.totalPrice(); }
Do Now!
Did you notice how similar this method body is to the one above for count? In Fundies 1, we had a terser way of expressing this sort of computation. What kind of operation on lists are we computing here? We will see in Lecture 13: Abstracting over behavior how to improve this code.
allBefore
In the template, this.rest.allBefore(year) produces a list of all books in the rest of this list published before the given date. The only work that remains is to decide whether the first book of this list belongs in the output list, and either add it to the result or not. If only we could determine whether that first Book was published before the given year! (Look carefully at the template: we cannot access this.book.year, because we do not have access to fields of fields.) So we add a method to our wish list, and we will delegate the job of deciding this question to the Book class itself. The method body in the class ConsLoBook becomes:
// produce a list of all books published before the given date // from this empty list of books ILoBook allBefore(int year) { if (this.first.publishedBefore(year)) { return new ConsLoBook(this.first, this.rest.allBefore(year)); } else { return this.rest.allBefore(year); } }
if (some condition) { //...statements to execute if condition was true... } else { //...statements to execute if condition was false... }
We’re not quite done; we have a method remaining on our wish list, so we must add to the class Book the method
// was this book published before the given year? boolean publishedBefore(int year) { return this.year < year; }
Exercise
Flesh out the rest of the design of this method, adding examples and tests.
Of course, for all of these methods, we end the design process by making sure all tests run. The actual test methods will be:
// tests for the method count boolean testCount(Tester t) { return t.checkExpect(this.mtlist.count(), 0) && t.checkExpect(this.lista.count(), 1) && t.checkExpect(this.listd.count(), 3); } // tests for the method totalPrice boolean testTotalPrice(Tester t) { return t.checkInexact(this.mtlist.totalPrice(), 0.0, 0.001) && t.checkInexact(this.lista.totalPrice(), 10.0, 0.001) && t.checkInexact(this.listc.totalPrice(), 95.0, 0.001) && t.checkInexact(this.listd.totalPrice(), 95.0, 0.001); } // tests for the method allBefore boolean testAllBefore(Tester t) { return t.checkExpect(this.mtlist.allBefore(2001), this.mtlist) && t.checkExpect(this.lista.allBefore(2001), this.lista) && t.checkExpect(this.listb.allBefore(2001), this.mtlist) && t.checkExpect(this.listc.allBefore(2001), new ConsLoBook(this.lpp, new ConsLoBook(this.ll, this.mtlist))) && t.checkExpect(this.listd.allBefore(2001), new ConsLoBook(this.ll, new ConsLoBook(this.lpp, this.mtlist))); }
5.3 Sorting
The last method to design was defined in the interface ILoBook as:
// produce a list of all books in this list, sorted by their price ILoBook sortByPrice();
An empty list is sorted already, so in the class MtLoBook the method becomes:
// produce a list of all books in this list, sorted by their price public ILoBook sortByPrice() { return this; }
We do not need to create a new empty list, this one works perfectly well.
We need examples for the more complex cases. We recall our sample data:
//Books Book htdp = new Book("HtDP", "MF", 2001, 60); Book lpp = new Book("LPP", "STX", 1942, 25); Book ll = new Book("LL", "FF", 1986, 10); // lists of Books ILoBook mtlist = new MtLoBook(); ILoBook lista = new ConsLoBook(this.lpp, this.mtlist); ILoBook listb = new ConsLoBook(this.htdp, this.mtlist); ILoBook listc = new ConsLoBook(this.lpp, new ConsLoBook(this.ll, this.listb)); ILoBook listd = new ConsLoBook(this.ll, new ConsLoBook(this.lpp, new ConsLoBook(this.htdp, this.mtlist))); ILoBook listdUnsorted = new ConsLoBook(this.lpp, new ConsLoBook(this.htdp, new ConsLoBook(this.ll, this.mtlist)));
and our tests will be:
// test the method sort for the lists of books boolean testSort(Tester t) { return t.checkExpect(this.listc.sort(), this.listd) && t.checkExpect(this.listdUnsorted.sort(), this.listd); }
Next we look at the template that is relevant for this question:
/* TEMPLATE: --------- Fields: ... this.first ... -- Book ... this.rest ... -- ILoBook Methods: ... this.sort() ... -- ILoBook Methods for Fields: ... this.rest.sort() ... -- ILoBook */
Do Now!
When we do get to it, where should this helper method be defined?
// In ConsLoBook // produces a list of the books in this non-empty list, sorted by price public ILoBook sortByPrice() { return this.rest.sortByPrice() // sort the rest of the list... .insert(this.first); // and insert the first book into that result }
Do Now!
Implement insert for ConsLoBook. Pay careful attention to the use of the template to guide your recursive calls.
// in ConsLoBook // insert the given book into this list of books // already sorted by price public ILoBook insert(Book b) { if (this.first.cheaperThan(b)) { return new ConsLoBook(this.first, this.rest.insert(b)); } else { return new ConsLoBook(b, this); } }
Do Now!
Why? What did we forget? (Hint: if you try this code in Eclipse, where does it indicate there are errors?)
// in Book // is the price of this book cheaper than the price of the given book? boolean cheaperThan(Book that) { return this.price < that.price; }
But still we have a problem. We’ve defined insert on ConsLoBook, but
in the third line, we write this.rest.insert(b) —
// in ILoBook // insert the given book into this list of books // already sorted by price ILoBook insert(Book b);
Do Now!
Now what did we miss?
This is another, subtle example of the benefits of writing down our types explicitly. In DrRacket, if we tried to define a function over a union type, and forgot a case, the only way we’d find out is if a test caught the lapse. Here, Java can immedately warn us that we’ve forgotten something
// in MtLoBook // insert the given book into this empty list of books // already sorted by price public ILoBook insert(Book b) { return new ConsLoBook(b, this); }
And now we are finally done!
This sorting order is called “lexicographic”, and is the generalization of sorting alphabetically to account for digits, punctuation, other alphabets, and all the other characters allowed in strings.
Exercise
Suppose we wanted to sort the books by title, instead of by price. We cannot use the < operator to compare Strings. Instead, Strings have a method compareTo(String) that returns:
-1 if this String is lexicographically before the given String
0 if the strings are lexicographically equal
1 if this String is lexicographically after the given String
Use this method to define a method titleBefore on Books, analogous to cheaperThan, and revise sort and/or insert to use it.