Lecture 6: Programming, Objectively
Overview
More examples of classes, objects, and methods
Strings are objects, and methods on Strings
From examples to testing
6.1 More Classes
This example is from Ben Lerner http://www.ccs.neu.edu/course/cs2510/lecture3.html.
As we mentioned in the last section, there is a wide variety of data in the world that composes multiple simple values. We talked about Points in class, and Student records in the last set of notes. Let’s take another example, modeling an inventory of books. A simple representation of books would need to cover at least their title, author, and price. That’s straightforward to describe as a class:
Do Now!
Translate the English description above into a class definition.
class Book { String title; String author; int price; Book(String title, String author, int price) { this.title = title; this.author = author; this.price = price; } }
The type String is a natural starting point for title and author, and the price is certainly some kind of number. Again, we see that we write the field definitions for title, author, and price without any values (they are uninitialized field definitions), and we include the same shape of constructor we talked about already, that has one this.someName = someName line for each field/parameter.
Let’s write some methods for Book. Imagine the store has periodic sales, where everything is discounted at particular rates. Write a function salePrice that takes a number representing a percentage (e.g. between 0 and 100), and returns a number representing the price with that percentage subtracted.
Do Now!
Try it yourself before reading on.
Following the recipe, we’ll first specify the method header, which we’ll write inside the Book class:
/* @param percentage A percent (between 0 and 100) of the discount to subtract @return The price with the discount subtracted */ int salePrice(int percentage) { }
We should also write some examples, both of Books and of the method’s use:
class ExamplesBook { Book schemer = new Book("The Little Schemer", "Daniel P. Friedman", 40); Book stick = new Book("Make it Stick: The Science of Succesful Learning", "Peter C. Brown", 13); Book pLaw = new Book("Parkinson's Law", "C. Northcote Parkinson", 30); int sale1 = this.schemer.salePrice(25); // should be 30 int sale2 = this.stick.salePrice(50); // should be 7 int sale3 = this.pLaw.salePrice(10); // should be 27 }
This gives us a pretty good sense of how we’ll get to the result; we’ll have a subtraction and a multipication. Since we’re using ints, we need to be a bit careful about the order of operations:
/* @param percentage A percent (between 0 and 100) of the discount to subtract @return The price with the discount subtracted */ int salePrice(int percentage) { return this.price - (this.price * percentage) / 100; }
We can save this all in a file called ExamplesBook.java, run it, and confirm for ourselves that it runs as we expect. There are a number of ways we can extend this class. We might try to add more authors (the first two examples above actuall have multiple authors). We might add a field called subtitle, since the full title of "Make it Stick..." is a bit long. We could also add some new methods. We could ask, for instance, if two books have the same author. That would be a method on Book that needs to take in another Book reference, and compare the author fields somehow. Let’s work through this next.
Do Now!
What should we make sure to add when writing examples for this method?
/* @param other The Book to compare against @return true if this book and other have the same author, false otherwise */ boolean sameAuthor(Book other) { } class ExamplesBook { // (omitting the examples from before, imagine it's here) Book reason = new Book("The Reasoned Schemer", "Daniel P. Friedman", 38); boolean same1 = this.reason.sameAuthor(this.schemer); // should be true boolean same2 = this.reason.sameAuthor(this.stick); // should be false boolean same3 = this.schemer.sameAuthor(this.reason); // should be true }
Actually, for this specific example, it will work. But for more sophisticated uses of Strings, it won’t, so we’ll tackle the general solution now.
Interlude – String is a class
String is actually a class, and the values we’ve been using as Strings are actually objects! The == won’t work the way we expect here on objects (we’ll talk more about why later). This explains why we write String with a capital S – the convention for class names is that they start with a capital letter (as opposed to simple, or primitive, types like int and boolean).
It also helps explain how we can go about performing operations other than + on Strings. Since they are objects, we ought to be able to call methods on them! The String class defines a method .equals which can be used to compare one string to another to check if they have the same contents. That gives us what we need to finish off the method body of sameAuthor:
boolean sameAuthor(Book other) { return this.author.equals(other.author); }
If we run this, we get the answer we expect:
new ExamplesBook:1( |
this.schemer = |
new Book:2( |
this.title = "The Little Schemer" |
this.author = "Daniel P. Friedman" |
this.price = 40) |
this.stick = |
new Book:3( |
this.title = "Make it Stick: The Science of Succesful Learning" |
this.author = "Peter C. Brown" |
this.price = 13) |
this.pLaw = |
new Book:4( |
this.title = "Parkinson's Law" |
this.author = "C. Northcote Parkinson" |
this.price = 30) |
this.reason = |
new Book:5( |
this.title = "The Reasoned Schemer" |
this.author = "Daniel P. Friedman" |
this.price = 38) |
this.same1 = true |
this.same2 = false |
this.same3 = true) |
There are a few things worth noting about our new realization that String is a class, and String values are objects:
When printed, strings don’t print as new String:5("some string"), the way other objects do. This is mainly for convenience, since they are relatively common and the output would be difficult to read otherwise.
The operator + is pretty special, since it is able to work with both primitives like int and references to objects like Strings. Not many other features in Java work over so many different kinds of values.
We’ll slowly learn more methods on Strings that we can use to do more and more string operations. They are all listed in Java’s official documentation for String, if you want to look them up yourself.
To introduce another few String methods and practice with them, let’s write one more function. Sometimes when printing an inventory, we might have long titles, like the full title of "Make it Stick", that don’t fit in a column width-wise. A common trick is to truncate the string down to a certain length, and replace the end with "...". If the string is below the given length, we leave it alone. So let’s write truncateTitle, which will take a length cut the title down to, and return the truncated title.
/* @param length The number of characters to appear before "..." @return A new string containing length characters followed by "..." if the string was too long, or the original string otherwise */ String truncateTitle(int length) { } class ExamplesBook { // examples from above omitted here String truncate1 = this.stick.truncateTitle(15); // Should be "Make it Stick: ..." String truncate2 = this.schemer.truncateTitle(20); // Should be "The Little Schemer" String truncate3 = this.schemer.truncateTitle(0); // Should be "..." }
We’ll need two String methods to do this:
length, which takes no arguments and returns the number of characters in the String
substring, which takes two ints and returns the part of the string starting at the first and ending just before the second. The first position in the string is 0.
Here are some examples:
class ExamplesBook { // examples from above omitted here String ss1 = "abcd".substring(0, 1); // Produces "a" String ss2 = "abcd".substring(0, 4); // Produces "abcd" String ss3 = "Hello there".substring(0, 7); // Produces "Hello t" String l1 = "abcd".length(); // Produces 4 String l2 = "".length(); // Produces 0 String l3 = "The Little Schemer".length(); // Produces 18 }
By putting these together, we can write the body of truncate:
String truncateTitle(int length) { if(this.title.length() > length) { return this.title.substring(0, length) + "..."; } else { return this.title; } }
We put a few pieces together to make this happen. First, the description of the problem was conditional – we were to give one answer if the string was too long, and another if it was short enough. Second, we used length on the title field to compare it to the parameter to check which case needed to be processed. Third, we used the substring method to pick out the part of the string from the beginning (position 0), up to the specified length.
We often call the positions in a String the indices in the string. So the first character is at index 0.
Do Now!
What is the index of the last character, in terms of the string’s length?
6.2 From Examples to Testing
Let’s take stock of the whole program we’ve written so far in these notes:
class Book { String title; String author; int price; Book(String title, String author, int price) { this.title = title; this.author = author; this.price = price; } /* @param percentage A percent (between 0 and 100) of the discount to subtract @return The price with the discount subtracted */ int salePrice(int percentage) { return this.price - (this.price * percentage) / 100; } /* @param other The Book to compare against @return true if this book and other have the same author, false otherwise */ boolean sameAuthor(Book other) { return this.author.equals(other.author); } /* @param length The number of characters to appear before "..." @return A new string containing length characters followed by "..." if the string was too long, or the original string otherwise */ String truncateTitle(int length) { if(this.title.length() > length) { return this.title.substring(0, length) + "..."; } else { return this.title; } } } class ExamplesBook { Book schemer = new Book("The Little Schemer", "Daniel P. Friedman", 40); Book stick = new Book("Make it Stick: The Science of Succesful Learning", "Peter C. Brown", 13); Book pLaw = new Book("Parkinson's Law", "C. Northcote Parkinson", 30); Book reason = new Book("The Reasoned Schemer", "Daniel P. Friedman", 38); int sale1 = this.schemer.salePrice(25); // should be 30 int sale2 = this.stick.salePrice(50); // should be 7 int sale3 = this.pLaw.salePrice(10); // should be 27 String truncate1 = this.stick.truncateTitle(15); // Should be "Make it Stick: ..." String truncate2 = this.schemer.truncateTitle(20); // Should be "The Little Schemer" String truncate3 = this.schemer.truncateTitle(0); // Should be "..." boolean same1 = this.reason.sameAuthor(this.schemer); // should be true boolean same2 = this.reason.sameAuthor(this.stick); // should be false boolean same3 = this.schemer.sameAuthor(this.reason); // should be true }
Here’s what we get when we run it:
⤇ ./run ExamplesBook |
Tester Prima v.2.1 |
----------------------------------- |
Tests defined in the class: ExamplesBook: |
--------------------------- |
ExamplesBook: |
--------------- |
|
new ExamplesBook:1( |
this.schemer = |
new Book:2( |
this.title = "The Little Schemer" |
this.author = "Daniel P. Friedman" |
this.price = 40) |
this.stick = |
new Book:3( |
this.title = "Make it Stick: The Science of Succesful Learning" |
this.author = "Peter C. Brown" |
this.price = 13) |
this.pLaw = |
new Book:4( |
this.title = "Parkinson's Law" |
this.author = "C. Northcote Parkinson" |
this.price = 30) |
this.reason = |
new Book:5( |
this.title = "The Reasoned Schemer" |
this.author = "Daniel P. Friedman" |
this.price = 38) |
this.sale1 = 30 |
this.sale2 = 7 |
this.sale3 = 27 |
this.truncate1 = "Make it Stick: ..." |
this.truncate2 = "The Little Schemer" |
this.truncate3 = "..." |
this.same1 = true |
this.same2 = false |
this.same3 = true) |
--------------- |
No test methods found. |
Let’s observe a few things about this.
We should be very happy that we have examples that double-check the work we did while coding the methods we wrote. It gives us a lot of confidence that our code is working as intended.
It’s not the greatest to have to refer back and forth between the file we wrote and the output to verify that everything is as we expected. That is, we have to visually inspect that each output is correct, based on the comment we wrote with the example.
There’s a weird message at the bottom that says No test methods found. that we haven’t talked about yet.
Before we handle that last point, there’s one step we could take to make it easier to double-check our work right away. Instead of putting the answer to the method call directly into the example, we could use == (in the case of int and boolean), or .equals() (in the case of Strings) to check if the answer is what we expect. Then we would just need to make sure that all the examples evaluate to true, which is easier than comparing them with comments.
Specifically, what if we write the examples as:
boolean sale1 = this.schemer.salePrice(25) == 30; boolean sale2 = this.stick.salePrice(50) == 7; boolean sale3 = this.pLaw.salePrice(10) == 27; boolean truncate1 = this.stick.truncateTitle(15).equals("Make it Stick: ..."); boolean truncate2 = this.schemer.truncateTitle(20).equals("The Little Schemer"); boolean truncate3 = this.schemer.truncateTitle(0).equals("..."); boolean same1 = this.reason.sameAuthor(this.schemer) == true; boolean same2 = this.reason.sameAuthor(this.stick) == false; boolean same3 = this.schemer.sameAuthor(this.reason) == true;
Then we get this output for the examples portion:
this.sale1 = true |
this.sale2 = true |
this.sale3 = true |
this.truncate1 = true |
this.truncate2 = true |
this.truncate3 = true |
this.same1 = true |
this.same2 = true |
this.same3 = true) |
That’s much easier to check visually – they’re all true! However, it’s much more frustrating if we make a mistake. Say we made an error and truncateTitle somehow produced "Make it Stick:...." instead of "Make it Stick: ...". The output would just show false, rather than the actual output of the method for us to compare and understand our mistake.
Professional testing systems do much more than just check equality and print results, but it is always a core feature they support.
A detailed description of the tester library is at http://www.ccs.neu.edu/course/cs2510/tester-doc.html.
It’s useful to write this all out, and describe the pieces. Here’s a method we could add to replace the first example we wrote, sale1:
// Originally we wrote this: int sale1 = this.schemer.salePrice(25); // Should be 30 // We can write this instead: boolean testSale(Tester t) { return t.checkExpect(this.schemer.salePrice(25), 30); }
We also have to tell Java that we want to use the Tester class, since it isn’t built-in to Java (it’s part of a library we’re using). So this line is necessary at the top of the file whenever we mention Tester:
import tester.Tester;
If we run this version of the program, we get this new output at the end:
--------------- |
|
Ran 1 test. |
All tests passed. |
|
--- END OF TEST RESULTS --- |
This is the testing library telling us that it ran the test method, and the expected values were equal to one another. We can add more tests in two ways. First, we can use multiple calls to checkExpect together in the test method, and combine them together with &&:
// We can write this instead of all the sale examples: boolean testSale(Tester t) { return t.checkExpect(this.schemer.salePrice(25), 30) && t.checkExpect(this.stick.salePrice(50), 7) && t.checkExpect(this.pLaw.salePrice(10), 27); }
Note that each call to checkExpect counts as one “test” in the output.
--------------- |
|
Ran 3 tests. |
All tests passed. |
|
--- END OF TEST RESULTS --- |
Second, we can define as many test methods as we like. So we could define another that captures all the truncate tests, and another that captures all the same author tests:
boolean testSale(Tester t) { return t.checkExpect(this.schemer.salePrice(25), 30) && t.checkExpect(this.stick.salePrice(50), 7) && t.checkExpect(this.pLaw.salePrice(10), 27); } boolean testTruncateTitle(Tester t) { return t.checkExpect(this.stick.truncateTitle(15), "Make it Stick: ...") && t.checkExpect(this.schemer.truncateTitle(20), "The Little Schemer") && t.checkExpect(this.schemer.truncateTitle(0), "..."); } boolean testSameAuthor(Tester t) { return t.checkExpect(this.reason.sameAuthor(this.schemer), true) && t.checkExpect(this.reason.sameAuthor(this.stick), false) && t.checkExpect(this.schemer.sameAuthor(this.reason), true); }
Now this produces:
--------------- |
|
Ran 9 tests. |
All tests passed. |
|
--- END OF TEST RESULTS --- |
This nicely summarizes all the successes; in addition, the testing library is smart enough to correctly compare Strings with .equals and ints with ==, so we don’t have to worry about that detail. In addition, if we get something wrong, it’s quite helpful in pointing out how. For example, say we accidentally used (percentage / 100) (which rounds to 0) in the implementation of salePrice. Here’s the output we would get:
--------------- |
|
Ran 7 tests. |
1 test failed. |
|
Failed test results: |
-------------- |
|
Error in test number 1 |
|
tester.ErrorReport: Error trace: |
at ExamplesBook.testSale(ExamplesBook.java:54) |
at java.util.concurrent.FutureTask.run(FutureTask.java:266) |
actual:...........................................expected: |
|
40................................................30 |
|
|
--- END OF TEST RESULTS --- |
This prints the result of calling the method on the left, and on the right, the result we originally wrote down as the expected answer in the test. This helps us quickly see which test has failed.
It’s also important to note the counts of tests that ran – the message says that 7 tests ran and 1 failed (so 6 of them passed), but we wrote 9 tests! That’s because a test method stops when the first test within it fails. In this case, the first check inside testSale failed, so the other two didn’t run. That accounts for the difference. Often, this is useful because it makes the test output not be overwhelming and report all the different ways things are failing, since often multiple tests are failing for the same reason. If we really want tests to run even when others fail, we can always put them into separate test methods.
6.3 Summary
We saw another example of a class with Book, and some more methods.
We learned that String is a class, and has many useful methods defined on it, including .equals, which is used to compare Strings for equality.
We saw how we can turn our examples into tests, which have some nice features for summarizing our examples and reporting mistakes.