On this page:
Lecture outline
Designing methods for simple classes
Evaluation of arithmetic expressions
Methods for classes with containment:   Designing method templates
Methods that produce objects
6.2.1

3 Lecture 3: Methods for simple classes

Design methods for simple classes of data.
Design methods for classes that contain objects of another class. Wish lists.

Related files:
  Book.java     BookAuthor.java     Book2.java  

Lecture outline
Designing methods for simple classes

This code is in the file Book.java

We want to represent an book with an author as data in DrRacket and then in Java.

Here is the data definition in DrRacket, as well as examples of data:

;; to represent a book in a bookstore
;; A Book is (make-book String String Number)
(define-struct book (title author price))
 
;; Examples:
(define htdp (make-book "HtDP" "FFFK" 60))
(define beaches (make-book "Beaches" "PC" 20))

We now want to define a function that will produce the book price on the day we have a sale. On the sale day, all books are discounted by the same percentage. Following the Design Recipe we produce the following function and tests:

;; compute the sale price of a book for the given discount
;; sale-price: Book Num -> Num
(define (sale-price abook disc)
  (- (book-price abook) (/ (* (book-price abook) disc) 100)))
 
(check-expect (sale-price htdp 30) 42)
(check-expect (sale-price beaches 20) 16)

Note that this function is useless for other types of data - they require that we provide a book.

In Java all computations relevant for one type of data are defined as methods in the class that represents the relevant data. (We say that the methods represent the behavior of the objects that are the members of this class.)

We start by rewriting the above data definitions as a class diagram and defining the classes that represent books and examples of data and computation relavent for books:

The class diagram and the Java class definitions for the class of books is straightforward:

+---------------+

| Book          |

+---------------+

| String title  |

| String author |

| int price     |

+---------------+

To define the method, we follow the same Design Recipe as before. Rather than tracing the steps of the Design Recipe we show the complete program together with the test suite and add comments that explain how the whole program has been built.

The class definition starts with the constructor, the template is written once for the whole class and comes right after the constructor (because it grows as more methods are added to the class definition, and always involves all fields defined in the class). The individual method definitions come after that.

Every method definition consists of the following parts:

Here is the Java code:

// to represent a book in a bookstore class Book {
String title;
String author;
int price;
 
String STR_CONST = 40;
 
// the constructor Book(String title, String author, int price) {
this.title = title;
this.author = author;
this.price = price;
}
 
/* TEMPLATE: ... this.title ... -- String ... this.author ... -- String ... this.price ... -- int   Note that the template includes all fields defined in this class and specifies their types as well. We will extend the template in the subsequent examples. */
 
/* The method definitions come here. */
}
We follow the design recipe.

We start with a comment, a purpose statement, then write the contract and the header. Here the contract and the header are combined into one line that specifies the name of the method, the type of value that the method will produce, and includes not only the list of argument names, but for each it specifies its data type as well.

There is no argument of the type Book included in the list of arguments - because only objects of the type Book are allowed to ask what is their sale price. We say that an instance of the Book class (or object of the type Book ) invokes the methods defined in the Book class. This object acts as an implicit argument for the method. Within the body of the method we refer to the object that invoked the method by the keyword this, and can access individual fields for this book as this.title or this.price, etc. (These are the selectors, simlar to what in DrRacket was written as (book-title abook) or (book-price abook)

The examples are shown below and show that each method evaluation request is preceeded by the book object for which the computation should be made.

The helps us in building the template – shown above. All methods defined in the Book class will have the same basic template - and so we write the template as a block comment right after the constructor for this class, before all method definitions.

Back to the actual code: here is the method definition:

// compute the sale price of this book given today's discount // in percent of the priginal price int salePrice(int discount) {
return this.price - (this.price * discount) / 100;
}

The Examples... class contains both the data definitions for our class and the tests for the method we just defined (of course, we only can test a method if we have the needed data).

// examples and tests for the class hierarchy that represents // books and authors class ExamplesBooks {
ExamplesBooks() {}
 
// examples of boooks Book htdp = new Book("HtDP", "FFK", 60);
Book beaches = new Book("Beaches", "PC", 20);
 
/* The tests are defined as methods that produce a boolean value that indicates whether the tests passed. We need to add an 'import' statement on the top of the file that indicates that we will use the tester library. The library evaluates the checkExpect methods that consume two values and produces true or false depending on whether the two values are the same. */
 
// test the method salePrice for the class Book boolean testSalePrice(Tester t) {
return
t.checkExpect(this.htdp.salePrice(30), 42) &&
t.checkExpect(this.beaches.salePrice(20), 16);
}
 
/* We can now run our program and see the test results. */
}
Evaluation of arithmetic expressions

The method body contained the following formula:

this.price - (this.price * discount) / 100

Well, for similar expressions in DrRacket we knew exactly how they will be evaluated. Every function invocation was enclosed in parentheses, with the first item after the opening left parentheses representing the function to evaluate, with the remaining items prior to the closing right parentheses representing the arguments for the function. And the evaluation started with the inner-most set of parentheses.

In Java, the arithmetic operators try to mimic the conventions used in mathematical formulas by assigning the order of preference to the operations. The multiplication and addition takes precedence over addition or subtraction and so this expression is evaluated as if the parentheses were given this way:

(this.price - ((this.price * discount) / 100))

Additonally, operations at the same level of preference are evaluated left-to-right, and os this expression can also be written as:

this.price - this.price * discount / 100

Savvy programmers add extra parentheses whenever the order of operations is not clear, or to emphasise the meaning of the formula. (Unlike Racket, adding parentheses does not mean “invoke this value as a function”, since Java does not use parenthetical syntax. So adding parentheses for clarity is semantically just fine.)

Methods for classes with containment: Designing method templates

This code is in the file BookAuthor.java

Let us continue with the example that included a book and the author:

// to represent a book in a bookstore class Book {
String title;
Author author;
int price;
 
// the constructor Book(String title, Author author, int price) {
this.title = title;
this.author = author;
this.price = price;
}
}
 
// to represent a author of a book in a bookstore class Author {
String name;
int yob;
 
// the constructor Author(String name, int yob) {
this.name = name;
this.yob = yob;
}
}
 
 
// examples and tests for the class hierarchy that represents // books and authors class ExamplesBooks {
ExamplesBooks() {}
 
// sample author and a book Author pat = new Author("Pat Conroy", 1948);
Book beaches = new Book("Beaches", this.pat, 20);
}

We would like to know if the authors of two books are the same. We follow the design recipe in designing this method. In DrRacket the function would have the purpose and header as follows:

;; are the two given books by the same author?
;; same-author? : Book Book -> Boolean
(define (same-author? book1 book2)...)

In Java, the first book becomes the implicit argument, the instance which invokes the method, and the second book will be the sole argument for the method. Here is the purpose and the header:

// is this book written by the same author as the given book? boolean sameAuthor(Book that) {...}

We need examples:

// examples of authors Author pat = new Author("Pat Conroy", 1948);
Author dan = new Author("Dan Brown", 1962);
 
// examples of books Book beaches = new Book("Beaches", this.pat, 20);
Book prince = new Book("Prince of Tides", this.pat, 15);
Book code = new Book("Da Vinci Code", this.dan, 20);
 
// test the method sameAuthor in the class Book boolean testSameBookAuthor(Tester t) {
return
t.checkExpect(this.beaches.sameAuthor(this.prince), true) &&
t.checkexpect(this.beaches.sameAuthor(this.code), false);
}

Now we look at the template. The common template for all methods in the class Book looks like this:

/* TEMPLATE: Fields: ... this.title ... -- String ... this.author ... -- Author ... this.price ... -- int   Methods: ... this.salePrice(int) ... -- int ... this.sameAuthor(Book) ... -- boolean   Methods for fields: ... this.author.mmm(??) ... -- ?? */

We first add all methods defined for the Book class. That includes this.salePrice(int), defined earlier, as well as this.sameAuthor(Book) we are defining now. So, for example, our method this.sameAuthor could invoke this.salePrice - though for this method it is useless.

If one of the fields in the class is an instance of another class, we add to the template all methods defined for that class, as they can be invoked by that field. Here we would add any methods defined for the Author class — that can be invoked by this.author.

Just as in DrRacket, we then add any information we can extract from the parameters of the method, in this case, any information that that book can provide. It could invoke the methods in its defining class, and, if the argument is of the same type as the class where the method is defined, we can also access all of its fields.

So, our template for this method expands to:

/* TEMPLATE: Fields: ... this.title ... -- String ... this.author ... -- Author ... this.price ... -- int   Fields for the method parameters: ... that.title ... -- String ... that.author ... -- Author ... that.price ... -- int   Methods: ... this.salePrice(int) ... -- int ... this.sameAuthor(Book) ... -- boolean   Methods invoked by the method parameters: ... that.salePrice(int) ... -- int ... that.sameAuthor(Book) ... -- boolean   Methods for fields: ... this.author.mmm(??) ... -- ?? ... that.author.mmm(??) ... -- ?? */

Trying to finish the design of the method we see that we do not have enough information available. We need to know whether the authors are the same — but only the Author class can define the method that would compare two authors.

Do Now!

What information isn’t available to the Book class, that would be needed to compute whether two authors are the same? Why isn’t that information available?

So, we make a wish list: we need to define the method sameAuthor in the Author class:

// is this the same author as the given one? boolean sameAuthor(Author that) {...}

We can now add this method to the template:

/* Methods for fields: ... this.author.sameAuthor(Author) ... -- boolean ... that.author.sameAuthor(Author) ... -- boolean */

and the method body becomes:

// is this book written by the same author as the given book? boolean sameAuthor(Book that) {
return this.author.sameAuthor(that.author);
}

Of course, before we test this method, we need to finish designing the method in our wish list:

// to represent a author of a book in a bookstore class Author {
String name;
int yob;
 
// the constructor Author(String name, int yob) {
this.name = name;
this.yob = yob;
}
 
/* TEMPLATE Fields: ... this.name ... -- String ... this.yob ... -- int   Methods: ... this.sameAuthor(Author) ... -- boolean */
 
// is this the same author as the given one? boolean sameAuthor(Author that) {
return this.name.equals(that.name) &&
this.yob == that.yob;
}
}

with the added tests:

// test the method sameAuthor in the class Author boolean testSameAuthor(Tester t) {
return
t.checkExpect(
this.pat.sameAuthor(new Author("Pat Conroy", 1948)),
true) &&
t.checkexpect(this.pat.sameAuthor(this.dan), false);
}

This pattern of asking another object to “help”, by invoking a method on that object which requires access to information that only it actually has access to, is called delegation, and we’ll see a lot more of this pattern in the upcoming lectures.

Methods that produce objects

This code is in the file Book2.java

The methods so far produced results of the primitive type. We now look at how to design methods that produce objects.

Suppose the bookstore want to permanently decrease the price of all books by 20%. We need a method that produces a book with the price redced as desired.

The purpose and header will be:

// produce a book like this one, but with the price reduced by 20% Book reducePrice(){...}

Here are some examples:

// examples of books Book htdp = new Book("HtDP", "FFK", 60);
Book beaches = new Book("Beaches", "PC", 20);
 
// test the method reducePrice for the class Book boolean testReducePrice(Tester t) {
return
t.checkExpect(this.htdp.reducePrice(),
new Book("HtDP", "FFK", 48)) &&
t.checkExpect(this.beaches.reducePrice(),
new Book("Beaches", "PC", 16));
}

We see that the result is a new object and so the method body will contain return new Book(...). The template is as follows:

/* TEMPLATE: Fields: ... this.title ... -- String ... this.author ... -- String ... this.price ... -- int   Methods: ... this.salePrice(int) ... -- int ... this.reducePrice() ... -- Book */

We see that we can use the method defined earlier in the body of our method as follows:

// produce a book like this one, but with the price reduced by 20% Book reducePrice() {
return new Book(this.title, this.author, this.salePrice(20));
}

Of course, we finish by running the tests. Notice that the test cases now compare the values of two objects, not just the values of data of the primitive types.