411 REFERENCE · Wikibooks
The Way of the Java/Create your own objects
Reference material by Wikibooks contributors. This retains its source identity and is not 411 news reporting.
Create your own objects
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 hello, 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 the new command to create any Hello
objects, but we could have!
That example may not make any sense, since there is no
reason to create a Hello object, and it is not clear
what it would be good for if we did. In this chapter, we
will look at some examples of class definitions that create
useful new 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; hence, it
is an instance of some class.
When you invoke the new command 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.
Typically all the methods that operate on a type go in the
class definition for that type.
Here are some syntax issues about class definitions:
Class names (and hence object types) always 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 they will not be executed.
With those issues out of the way, let's look at an example of
a user-defined type, Time.
Time
A common motivation for creating a new Object type is to take several
related pieces of data and encapsulate them into an object that can be
manipulated (passed as an argument, operated on) as a single unit. We
have already seen two built-in types like this, Point and
Rectangle.
Another example, which we will implement ourselves, is Time,
which is used to record the time of day. The various pieces of
information that form a time are the hour, minute and second. Because
every Time object will contain these data, we need to create
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, so
we can record fractions of a second.
Instance variables are declared at the beginning of the class
definition, outside of any method definition, like this:
class Time
int hour, minute;
double second;
All by itself, this code fragment is a legal class definition.
After declaring the instance variables, the next step is usually
to define a constructor for the new class.
Constructors
The usual role of a constructor is to initialize the 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;
Notice that 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, as indicated by the
empty parentheses (). Each line of the constructor initializes
an instance variable to an arbitrary default value (in this case,
midnight). The name this is a special
keyword that is the name of 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 do not use new
to create it. In fact, you are not even allowed to make an
assignment to it! this is created by the system; all you
have to do is store values in its instance variables.
A common error when writing constructors is to put a return
statement at the end. Resist the temptation.
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 the new
command with the parameters of the constructors.
It is very common to have one constructor that takes no
arguments (shown above), and one constructor that takes
a parameter list that is 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 exactly 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 go back and look at the documentation for Points
and Rectangles, you will see that both classes provide
constructors like this. Overloading constructors provides the
flexibility to create an object first and then fill in the
blanks, or to collect all the information before creating
the object.
So far this might not seem very interesting, and in fact it
is not. Writing constructors is a boring, mechanical process.
Once you have written two, you will find that you can churn them
out in your sleep, just by looking at the list of instance
variables.
Creating a new object
Although constructors look like methods, you never invoke them
directly. Instead, when you use the new command, the system
allocates space for the new object and then
invokes your constructor to initialize the instance variables.
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;
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);
As an exercise, figure out the flow of execution through this
program.
In main, the first time we invoke the new command,
we provide no arguments, so Java invokes the first constructor.
The next few lines assign values to each of the instance
variables.
The second time we invoke the new command, we provide
arguments that match the parameters 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.
Printing an object
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.
In order to print objects in a way that is more meaningful to users
(as opposed to programmers), you usually want to 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 time.
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 very 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, though, you can take a look
at the documentation for the Date class in the
java.util package.
Operations on objects
Even though we can't print times in an optimal format, we can still
write methods that manipulate Time objects. In the next few
sections, I will demonstrate several of the possible interfaces for
methods that operate on objects. For some operations, you will have a
choice of several possible interfaces, so you should consider the pros
and cons of each of these:
pure function: Takes objects and/or primitives as
arguments but does not modify the objects. 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. 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.
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.
One example is after, which compares two Times and
returns a boolean that indicates whether the first operand
comes after the second:
public static boolean after (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);
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 second, corrected version of this 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 will suggest an alternate approach to this problem that
will be much shorter.
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 statement sum.second -= 60.0;
is equivalent to sum.second = sum.second - 60;
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 simply 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?
Fill-in methods
Occasionally you will see methods like addTime written
with a different interface (different arguments and return values).
Instead of creating a new object every time addTime is
invoked, we could require the caller to provide an empty
object where addTime should store the result. Compare
the following with 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;
One advantage of this approach is that the caller has the option
of reusing the same object repeatedly to perform a series
of additions. This can be slightly more efficient, although
it can be confusing enough to cause subtle errors.
For the vast majority of programming, it is worth
spending a little run time to avoid a lot of debugging time.
Which is best?
Anything that can be done with modifiers and fill-in methods can also
be done with pure functions. In fact, there are programming
languages, called functional programming languages, that only
allow pure functions. Some programmers believe that programs that use
pure functions are faster to develop and less error-prone than
programs that use modifiers. Nevertheless, there are times when
modifiers are convenient, and some cases where functional programs
are less efficient.
In general, I recommend that you write pure functions whenever
it is reasonable to do so, and resort to modifiers only if there
is a compelling advantage. This approach might be called a
functional programming style.
Incremental development vs. planning
In this chapter I have demonstrated an approach to program
development I refer to as rapid prototyping with iterative
improvement. In each case, I wrote a rough draft (or prototype)
that performed the basic calculation, and then tested it on
a few cases, correcting flaws as I found them.
Although this approach can be effective, 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 high-level planning, in which a little insight
into the problem can make the programming much 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.
Thus an alternate 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);
secs -= this.minute * 60;
this.second = secs;
This constructor is a little different from the others, since
it involves some calculation along with assignments to the
instance variables.
You might have to think a bit to convince yourself that the technique
I am using to convert from one base to another is correct. Assuming
you are 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 much 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.
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 more features later. For example, 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 is easier (fewer special cases, fewer opportunities for error).
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. I mentioned this word in
Chapter 1, but did not define it carefully. It is
not easy to define, so I will try a couple of approaches.
First, consider some things that are not algorithms. For example,
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 cheated by learning a few
tricks. For example, to find the product of and 9, you can
write as the first digit and 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 characteristics 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, intellectually 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.
Later you will have the opportunity to design
simple algorithms for a variety of problems.
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.
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.
project: A collection of one or more class definitions
(one per file) that make up a program.
startup class: The class that contains the main
method where execution of the program begins.
function: A method whose result depends only on its
parameters, and that has no side-effects other than returning
a value.
functional programming style: A style of program design
in which the majority of methods are functions.
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 parameter and fills in its instance variables instead
of generating a return value. This type of method is usually
not the best choice.
algorithm: A set of instructions for solving a class of
problems by a mechanical process.