Think Java: How to Think Like a Computer Scientist by Allen B. Downey - HTML preview

PLEASE NOTE: This is an HTML preview only and some elements such as links or page numbers may be incorrect.
Download the book in PDF, ePub, Kindle for a complete version.

Chapter 11

Create your own objects

11.1

Class definitions and object types

Every time you write a class definition, you create a new object type with

the same name as the class. Way back in Section 1.5, when we defined the

class named Hello, we also created an object type named Hello. We didn’t

create any variables with type Hello, and we didn’t use new to create any

Hello objects, but we could have!

That example doesn’t make much sense, since there is no reason to create

a Hello object, and it wouldn’t do much if we did. In this chapter, we will

look at class definitions that create useful object types.

Here are the most important ideas in this chapter:

❼ Defining a new class also creates a new object type with the same name.

❼ A class definition is like a template for objects: it determines what

instance variables the objects have and what methods can operate on

them.

❼ Every object belongs to some object type; that is, it is an instance of

some class.

❼ When you invoke new to create an object, Java invokes a special method

called a constructor to initialize the instance variables. You provide

one or more constructors as part of the class definition.

132

Chapter 11. Create your own objects

❼ The methods that operate on a type are defined in the class definition

for that type.

Here are some syntax issues about class definitions:

❼ Class names (and hence object types) should begin with a capital letter,

which helps distinguish them from primitive types and variable names.

❼ You usually put one class definition in each file, and the name of the

file must be the same as the name of the class, with the suffix .java.

For example, the Time class is defined in the file named Time.java.

❼ In any program, one class is designated as the startup class. The

startup class must contain a method named main, which is where the

execution of the program begins. Other classes may have a method

named main, but it will not be executed.

With those issues out of the way, let’s look at an example of a user-defined

class, Time.

11.2

Time

A common motivation for creating an object type is to encapsulate related

data in an object that can be treated as a single unit. We have already seen

two types like this, Point and Rectangle.

Another example, which we will implement ourselves, is Time, which repre-

sents the time of day. The data encapsulated in a Time object are an hour, a

minute and a number of seconds. Because every Time object contains these

data, we need instance variables to hold them.

The first step is to decide what type each variable should be. It seems clear

that hour and minute should be integers. Just to keep things interesting,

let’s make second a double.

Instance variables are declared at the beginning of the class definition, outside

of any method definition, like this:

index-153_1.png

11.3. Constructors

133

class Time {

int hour, minute;

double second;

}

By itself, this code fragment is a legal class definition. The state diagram for

a Time object looks like this:

After declaring the instance variables, the next step is to define a constructor

for the new class.

11.3

Constructors

Constructors initialize instance variables.

The syntax for constructors is

similar to that of other methods, with three exceptions:

❼ The name of the constructor is the same as the name of the class.

❼ Constructors have no return type and no return value.

❼ The keyword static is omitted.

Here is an example for the Time class:

public Time() {

this.hour = 0;

this.minute = 0;

this.second = 0.0;

}

134

Chapter 11. Create your own objects

Where you would expect to see a return type, between public and Time,

there is nothing. That’s how we (and the compiler) can tell that this is a

constructor.

This constructor does not take any arguments. Each line of the constructor

initializes an instance variable to a default value (in this case, midnight).

The name this is a special keyword that refers to the object we are creating.

You can use this the same way you use the name of any other object. For

example, you can read and write the instance variables of this, and you can

pass this as an argument to other methods.

But you do not declare this and you can’t make an assignment to it. this

is created by the system; all you have to do is initialize its instance variables.

A common error when writing constructors is to put a return statement at

the end. Resist the temptation.

11.4

More constructors

Constructors can be overloaded, just like other methods, which means that

you can provide multiple constructors with different parameters. Java knows

which constructor to invoke by matching the arguments of new with the

parameters of the constructors.

It is common to have one constructor that takes no arguments (shown above),

and one constructor that takes a parameter list identical to the list of instance

variables. For example:

public Time(int hour, int minute, double second) {

this.hour = hour;

this.minute = minute;

this.second = second;

}

The names and types of the parameters are the same as the names and types

of the instance variables. All the constructor does is copy the information

from the parameters to the instance variables.

If you look at the documentation for Points and Rectangles, you will see

that both classes provide constructors like this. Overloading constructors

11.5. Creating a new object

135

provides the flexibility to create an object first and then fill in the blanks, or

to collect all the information before creating the object.

This might not seem very interesting, and in fact it is not. Writing construc-

tors is a boring, mechanical process. Once you have written two, you will

find that you can write them quickly just by looking at the list of instance

variables.

11.5

Creating a new object

Although constructors look like methods, you never invoke them directly.

Instead, when you invoke new, the system allocates space for the new object

and then invokes your constructor.

The following program demonstrates two ways to create and initialize Time

objects:

class Time {

int hour, minute;

double second;

public Time() {

this.hour = 0;

this.minute = 0;

this.second = 0.0;

}

public Time(int hour, int minute, double second) {

this.hour = hour;

this.minute = minute;

this.second = second;

}

public static void main(String[] args) {

// one way to create and initialize a Time object

Time t1 = new Time();

t1.hour = 11;

136

Chapter 11. Create your own objects

t1.minute = 8;

t1.second = 3.14159;

System.out.println(t1);

// another way to do the same thing

Time t2 = new Time(11, 8, 3.14159);

System.out.println(t2);

}

}

In main, the first time we invoke new, we provide no arguments, so Java

invokes the first constructor. The next few lines assign values to the instance

variables.

The second time we invoke new, we provide arguments that match the param-

eters of the second constructor. This way of initializing the instance variables

is more concise and slightly more efficient, but it can be harder to read, since

it is not as clear which values are assigned to which instance variables.

11.6

Printing objects

The output of this program is:

Time@80cc7c0

Time@80cc807

When Java prints the value of a user-defined object type, it prints the name

of the type and a special hexadecimal (base 16) code that is unique for each

object. This code is not meaningful in itself; in fact, it can vary from machine

to machine and even from run to run. But it can be useful for debugging, in

case you want to keep track of individual objects.

To print objects in a way that is more meaningful to users (as opposed to

programmers), you can write a method called something like printTime:

public static void printTime(Time t) {

System.out.println(t.hour + ":" + t.minute + ":" + t.second);

}

Compare this method to the version of printTime in Section 3.10.

11.7. Operations on objects

137

The output of this method, if we pass either t1 or t2 as an argument, is

11:8:3.14159. Although this is recognizable as a time, it is not quite in the

standard format. For example, if the number of minutes or seconds is less

than 10, we expect a leading 0 as a place-keeper. Also, we might want to

drop the decimal part of the seconds. In other words, we want something

like 11:08:03.

In most languages, there are simple ways to control the output format for

numbers. In Java there are no simple ways.

Java provides powerful tools for printing formatted things like times and

dates, and also for interpreting formatted input. Unfortunately, these tools

are not very easy to use, so I am going to leave them out of this book. If

you want, you can take a look at the documentation for the Date class in the

java.util package.

11.7

Operations on objects

In the next few sections, I demonstrate three kinds of methods that operate

on objects:

pure function: Takes objects as arguments but does not modify them. The

return value is either a primitive or a new object created inside the

method.

modifier: Takes objects as arguments and modifies some or all of them.

Often returns void.

fill-in method: One of the arguments is an “empty” object that gets filled

in by the method. Technically, this is a type of modifier.

Often it is possible to write a method as a pure function, modifier, or a fill-in

method. I will discuss the pros and cons of each.

11.8

Pure functions

A method is considered a pure function if the result depends only on the

arguments, and it has no side effects like modifying an argument or printing

something. The only result of invoking a pure function is the return value.

138

Chapter 11. Create your own objects

One example is isAfter, which compares two Times and returns a boolean

that indicates whether the first operand comes after the second:

public static boolean isAfter(Time time1, Time time2) {

if (time1.hour > time2.hour) return true;

if (time1.hour < time2.hour) return false;

if (time1.minute > time2.minute) return true;

if (time1.minute < time2.minute) return false;

if (time1.second > time2.second) return true;

return false;

}

What is the result of this method if the two times are equal? Does that

seem like the appropriate result for this method? If you were writing the

documentation for this method, would you mention that case specifically?

A second example is addTime, which calculates the sum of two times. For

example, if it is 9:14:30, and your breadmaker takes 3 hours and 35 minutes,

you could use addTime to figure out when the bread will be done.

Here is a rough draft of this method that is not quite right:

public static Time addTime(Time t1, Time t2) {

Time sum = new Time();

sum.hour = t1.hour + t2.hour;

sum.minute = t1.minute + t2.minute;

sum.second = t1.second + t2.second;

return sum;

}

Although this method returns a Time object, it is not a constructor. You

should go back and compare the syntax of a method like this with the syntax

of a constructor, because it is easy to get confused.

Here is an example of how to use this method. If currentTime contains the

current time and breadTime contains the amount of time it takes for your

breadmaker to make bread, then you could use addTime to figure out when

the bread will be done.

Time currentTime = new Time(9, 14, 30.0);

11.8. Pure functions

139

Time breadTime = new Time(3, 35, 0.0);

Time doneTime = addTime(currentTime, breadTime);

printTime(doneTime);

The output of this program is 12:49:30.0, which is correct. On the other

hand, there are cases where the result is not correct. Can you think of one?

The problem is that this method does not deal with cases where the number

of seconds or minutes adds up to more than 60. In that case, we have to

“carry” the extra seconds into the minutes column, or extra minutes into the

hours column.

Here’s a corrected version of the method.

public static Time addTime(Time t1, Time t2) {

Time sum = new Time();

sum.hour = t1.hour + t2.hour;

sum.minute = t1.minute + t2.minute;

sum.second = t1.second + t2.second;

if (sum.second >= 60.0) {

sum.second -= 60.0;

sum.minute += 1;

}

if (sum.minute >= 60) {

sum.minute -= 60;

sum.hour += 1;

}

return sum;

}

Although it’s correct, it’s starting to get big. Later I suggest much shorter

alternative.

This code demonstrates two operators we have not seen before, += and -=.

These operators provide a concise way to increment and decrement variables.

They are similar to ++ and --, except (1) they work on doubles as well as

ints, and (2) the amount of the increment does not have to be 1. The state-

ment sum.second -= 60.0; is equivalent to sum.second = sum.second -

60;

140

Chapter 11. Create your own objects

11.9

Modifiers

As an example of a modifier, consider increment, which adds a given number

of seconds to a Time object. Again, a rough draft of this method looks like:

public static void increment(Time time, double secs) {

time.second += secs;

if (time.second >= 60.0) {

time.second -= 60.0;

time.minute += 1;

}

if (time.minute >= 60) {

time.minute -= 60;

time.hour += 1;

}

}

The first line performs the basic operation; the remainder deals with the

same cases we saw before.

Is this method correct? What happens if the argument secs is much greater

than 60? In that case, it is not enough to subtract 60 once; we have to

keep doing it until second is below 60. We can do that by replacing the if

statements with while statements:

public static void increment(Time time, double secs) {

time.second += secs;

while (time.second >= 60.0) {

time.second -= 60.0;

time.minute += 1;

}

while (time.minute >= 60) {

time.minute -= 60;

time.hour += 1;

}

}

This solution is correct, but not very efficient. Can you think of a solution

that does not require iteration?

11.10. Fill-in methods

141

11.10

Fill-in methods

Instead of creating a new object every time addTime is invoked, we could

require the caller to provide an object where addTime stores the result. Com-

pare this to the previous version:

public static void addTimeFill(Time t1, Time t2, Time sum) {

sum.hour = t1.hour + t2.hour;

sum.minute = t1.minute + t2.minute;

sum.second = t1.second + t2.second;

if (sum.second >= 60.0) {

sum.second -= 60.0;

sum.minute += 1;

}

if (sum.minute >= 60) {

sum.minute -= 60;

sum.hour += 1;

}

}

The result is stored in sum, so the return type is void.

Modifiers and fill-in methods are efficient because they don’t have to create

new objects. But they make it more difficult to isolate parts of a program;

in large projects they can cause errors that are hard to find.

Pure functions help manage the complexity of large projects, in part by

making certain kinds of errors impossible. Also, they lend themselves to

certain kids of composition and nesting. And because the result of a pure

function depends only on the parameters, it is possible to speed them up by

storing previously-computed results.

I recommend that you write pure functions whenever it is reasonable, and

resort to modifiers only if there is a compelling advantage.

142

Chapter 11. Create your own objects

11.11

Incremental development and planning

In this chapter I demonstrated a program development process called rapid

prototyping1. For each method, I wrote a rough draft that performed the

basic calculation, then tested it on a few cases, correcting flaws as I found

them.

This approach can be effective, but it can lead to code that is unnecessarily

complicated—since it deals with many special cases—and unreliable—since

it is hard to convince yourself that you have found all the errors.

An alternative is to look for insight into the problem that can make the

programming easier. In this case the insight is that a Time is really a three-

digit number in base 60! The second is the “ones column,” the minute is

the “60’s column”, and the hour is the “3600’s column.”

When we wrote addTime and increment, we were effectively doing addition

in base 60, which is why we had to “carry” from one column to the next.

Another approach to the whole problem is to convert Times into doubles

and take advantage of the fact that the computer already knows how to

do arithmetic with doubles. Here is a method that converts a Time into a

double:

public static double convertToSeconds(Time t) {

int minutes = t.hour * 60 + t.minute;

double seconds = minutes * 60 + t.second;

return seconds;

}

Now all we need is a way to convert from a double to a Time object. We

could write a method to do it, but it might make more sense to write it as a

third constructor:

public Time(double secs) {

this.hour =(int)(secs / 3600.0);

secs -= this.hour * 3600.0;

this.minute =(int)(secs / 60.0);

1What I am calling rapid prototyping is similar to test-driven development (TDD); the

difference is that TDD is usually based on automated testing. See http://en.wikipedia.

org/wiki/Test-driven_development.

11.12. Generalization

143

secs -= this.minute * 60;

this.second = secs;

}

This constructor is a little different from the others; it involves some calcu-

lation along with assignments to the instance variables.

You might have to think to convince yourself that the technique I am using

to convert from one base to another is correct. But once you’re convinced,

we can use these methods to rewrite addTime:

public static Time addTime(Time t1, Time t2) {

double seconds = convertToSeconds(t1) + convertToSeconds(t2);

return new Time(seconds);

}

This is shorter than the original version, and it is much easier to demonstrate

that it is correct (assuming, as usual, that the methods it invokes are correct).

As an exercise, rewrite increment the same way.

11.12

Generalization

In some ways converting from base 60 to base 10 and back is harder than

just dealing with times. Base conversion is more abstract; our intuition for

dealing with times is better.

But if we have the insight to treat times as base 60 numbers, and make

the investment of writing the conversion methods (convertToSeconds and

the third constructor), we get a program that is shorter, easier to read and

debug, and more reliable.

It is also easier to add features later. Imagine subtracting two Times to

find the duration between them. The naive approach would be to implement

subtraction complete with “borrowing.” Using the conversion methods would

be much easier.

Ironically, sometimes making a problem harder (more general) makes it easier

(fewer special cases, fewer opportunities for error).

144

Chapter 11. Create your own objects

11.13

Algorithms

When you write a general solution for a class of problems, as opposed to a

specific solution to a single problem, you have written an algorithm. This

word is not easy to define, so I will try a couple of approaches.

First, consider some things that are not algorithms. When you learned to

multiply single-digit numbers, you probably memorized the multiplication

table. In effect, you memorized 100 specific solutions, so that knowledge is

not really algorithmic.

But if you were “lazy,” you probably learned a few tricks. For example,

to find the product of n and 9, you can write n − 1 as the first digit and

10 − n as the second digit. This trick is a general solution for multiplying

any single-digit number by 9. That’s an algorithm!

Similarly, the techniques you learned for addition with carrying, subtraction

with borrowing, and long division are all algorithms. One of the charac-

teristics of algorithms is that they do not require any intelligence to carry

out. They are mechanical processes in which each step follows from the last

according to a simple set of rules.

In my opinion, it is embarrassing that humans spend so much time in school

learning to execute algorithms that, quite literally, require no intelligence.

On the other hand, the process of designing algorithms is interesting, intel-

lectually challenging, and a central part of what we call programming.

Some of the things that people do naturally, without difficulty or conscious

thought, are the most difficult to express algorithmically. Understanding

natural language is a good example. We all do it, but so far no one has been

able to explain how we do it, at least not in the form of an algorithm.

Soon you will have the opportunity to design simple algorithms for a variety

of problems.

11.14

Glossary

class: Previously, I defined a class as a collection of related methods. In this

chapter we learned that a class definition is also a template for a new

type of object.

11.15. Exercises

145

instance: A member of a class. Every object is an instance of some class.

constructor: A special method that initializes the instance variables of a

newly-constructed object.

startup class: The class that contains the main method where execution of

the program begins.

pure function: A method whose result depends only on its parameters, and

that has no side-effects other than returning a value.

modifier: A method that changes one or more of the objects it receives as

parameters, and usually returns void.

fill-in method: A type of method that takes an “empty” object as a pa-

rameter and fills in its instance variables instead of generating a return

value.

algorithm: A set of instructions for solving a class of problems by a me-

chanical process.

11.15

Exercises

Exercise 11.1. In the board game Scrabble2, each tile c