Section 5.3
Programming with Objects
THERE ARE SEVERAL WAYS in which
object-oriented concepts can be applied to the process of
designing and writing programs. The broadest of these is
object-oriented analysis and design
which applies an object-oriented methodology to the earliest
stages of program development, during which the overall design
of a program is created. Here, the idea is to identify
things in the problem domain that can be modeled as objects.
On another level, object-oriented programming encourages
programmers to produce generalized
software components that can be used in a wide variety
of programming projects.
Built-in Classes
Although the focus of object-oriented programming is generally
on the design and implementation of new classes, it's important not
to forget that the designers of Java have already provided a large
number of reusable classes. Some of these classes are meant to be
extended to produce new classes, while others can be used directly
to create useful objects. A true mastery of Java requires familiarity
with the full range of built-in classes -- something that takes a
lot of time and experience to develop. In the next chapter,
we will begin the study of Java's GUI classes, and you will encounter
other built-in classes throughout the remainder of this book. But
let's take a moment to look at a few built-in classes that you might find
useful.
A string can be built up from smaller pieces using the + operator,
but this is not very efficient. If str is a String and ch
is a character, then executing the command "str = str + ch;"
involves creating a whole new string that is a copy of str, with the value of ch
appended onto the end. Copying the string takes some time. Building up a long string
letter by letter would require a surprising amount of processing.
The class java.lang.StringBuffer makes it possible to be efficient
about building up a long string from a number of smaller pieces. Like a String,
a StringBuffer contains a sequence of characters. However, it is possible
to add new characters onto the end of a StringBuffer without making a copy
of the data that it already contains. If buffer is a variable of type
StringBuffer and x is a value of any type, then
the command buffer.append(x) will add x, converted into a
string representation, onto the end of the data that was already in the buffer.
This command actually modifies the buffer, rather than making a copy, and that
can be done efficiently. A long string can be built up in a StringBuffer
using a sequence of append() commands. When the string is complete,
the function buffer.toString() will return a copy of the string in
the buffer as an ordinary value of type String.
A number of useful classes are collected in the package java.util.
For example, this package contains classes for working with collections of
objects (one of the contexts in which wrapper classes for primitive types
are useful). We will study these collection classes in Chapter 12.
The class java.util.Date is used to represent times. When a Date
object is constructed without parameters, the result represents the current date and time, so
an easy way to display this information is:
System.out.println( new Date() );
Of course, to use the Date class in this way, you must make it available
by importing it with one of the statements "import java.util.Date;"
or "import java.util.*;" at the beginning of your program.
(See Section 4.5
for a discussion of packages and import.)
Finally, I will mention the class java.util.Random. An object
belonging to this class is a source of random numbers. (The standard
function Math.random() uses one of these objects behind the scenes
to generate its random numbers.) An object of type Random can generate
random integers, as well as random real numbers. If randGen is created
with the command:
Random randGen = new Random();
and if N is a positive integer, then randGen.nextInt(N)
generates a random integer in the range from 0 to N-1.
For example, this makes it a little easier to roll a pair of dice.
Instead of saying "die1 = (int)(6*Math.random())+1;",
one can say "die1 = randGen.nextInt(6)+1;". (Since you
also have to import the class java.util.Random and create the
Random object, you might not agree that it is actually easier.)
The main point here, again, is that many problems have already been solved,
and the solutions are available in Java's standard classes. If you are faced with
a task that looks like it should be fairly common, it might be worth looking through
a Java reference to see whether someone has already written a subroutine that
you can use.
Generalized Software Components
Every programmer builds up a stock of techniques and
expertise expressed as snippets of code that can be reused
in new programs using the tried-and-true method of
cut-and-paste: The old code is physically copied into the
new program and then edited to customize it as necessary.
The problem is that the editing is error-prone and
time-consuming, and the whole enterprise is dependent on
the programmer's ability to pull out that particular piece of
code from last year's project that looks like it might be
made to fit. (On the level of a corporation that wants to
save money by not reinventing the wheel for each new project,
just keeping track of all the old wheels becomes a major task.)
Well-designed classes are software components that can be
reused without editing. A well-designed class is not carefully
crafted to do a particular job in a particular program. Instead,
it is crafted to model some particular type of object or
a single coherent concept. Since objects and concepts can
recur in many problems, a well-designed class is likely to
be reusable without modification in a variety of projects.
Furthermore, in an object-oriented programming language,
it is possible to make subclasses
of an existing class. This makes classes even more reusable.
If a class needs to be customized, a subclass can
be created, and additions or modifications can be made in the
subclass without making any changes to the original class.
This can be done even if the programmer doesn't have access
to the source code of the class and doesn't know any details
of its internal, hidden implementation. We will discuss
subclasses in the next section.
Object-oriented Analysis and Design
A large programming project goes through a number of stages, starting
with specification of the problem
to be solved, followed by analysis
of the problem and design of
a program to solve it. Then comes coding,
in which the program's design is expressed in some actual
programming language. This is followed by testing
and debugging of the program.
After that comes a long period of maintenance,
which means fixing any new problems that are found in the program
and modifying it to adapt it to changing requirements. Together,
these stages form what is called the software
life cycle. (In the real world, the ideal of consecutive
stages is seldom if ever achieved. During the analysis stage, it might
turn out that the specifications are incomplete or inconsistent.
A problem found during testing requires at least a brief return to
the coding stage. If the problem is serious enough, it might even require
a new design. Maintenance usually involves redoing some of the work
from previous stages....)
Large, complex programming projects are only likely to succeed
if a careful, systematic approach is adopted during all stages
of the software life cycle. The systematic approach to programming,
using accepted principles of good design, is called
software engineering.
The software engineer tries to efficiently construct programs that
verifyably meet their specifications and that are easy to modify if
necessary. There is a wide range of "methodologies"
that can be applied to help in the systematic design of
programs. (Most of these methodologies seem to involve drawing
little boxes to represent program components, with labeled
arrows to represent relationships among the boxes.)
We have been discussing object orientation in programming
languages, which is relevant to the coding stage of program
development. But there are also object-oriented methodologies
for analysis and design. The question in this stage of the
software life cycle is, How can
one discover or invent the overall structure of a program?
As an example of a rather simple object-oriented approach to
analysis and design, consider this advice: Write down a
description of the problem. Underline all the nouns
in that description. The nouns should be considered as candidates
for becoming classes or objects in the program design. Similarly,
underline all the verbs. These are candidates for methods.
This is your starting point. Further analysis might uncover the
need for more classes and methods, and it might reveal that
subclassing can be used to take advantage of similarities among classes.
This is perhaps a bit simple-minded, but the idea is clear and
the general approach can be effective:
Analyze the problem to discover the concepts that are involved,
and create classes to represent those concepts. The design should
arise from the problem itself, and you should end up
with a program whose structure reflects the structure of
the problem in a natural way.
Programming Examples
The PairOfDice class in the previous section is
already an example of a generalized software component, although one that could
certainly be improved. The class represents a single, coherent concept,
"a pair of dice." The instance variables hold the data relevant
to the state of the dice, that is, the number showing on each of the dice. The
instance method represents the behaviour of a pair of dice, that is, the ability to
be rolled. This class would be reusable in many different programming projects.
On the other hand, the Student class from the previous section is
not very reusable. It seems to be crafted to represent students in a particular
course where the grade will be based on three tests. If there are more tests
or quizzes or papers, it's useless. If there are two people in the class who
have the same name, we are in trouble (one reason why numerical student ID's are
often used). Admittedly, it's much more difficult to develop a general-purpose
student class than a general-purpose pair-of-dice class. But this particular
Student class is good mostly as an example in a programming textbook.
Let's do another example in a domain that is simple enough that we have a
chance of coming up with something reasonably reusable. Consider card games
that are played with a standard deck of playing cards (a so-called
"poker" deck, since it is used in the game of poker). In a
typical card game, each player gets a hand of cards. The deck is shuffled and
cards are dealt one at a time from the deck and added to the players'
hands. In some games, cards can be removed from a hand, and new
cards can be added. The game is won or lost depending on the value (ace, 2, ..., king)
and suit (spades, diamonds, clubs, hearts) of the cards that a player receives.
If we look for nouns in this description, there
are several candidates for objects: game, player, hand, card, deck, value, and suit.
Of these, the value and the suit of a card are simple values, and they will just be
represented as instance variables in a Card object.
In a complete program, the other five nouns might be represented by classes.
But let's work on the ones that are most obviously reusable: card, hand, and deck.
If we look for verbs in the description of a card game, we see that
we can shuffle a deck and deal a card from a deck. This gives use us two
candidates for instance methods in a Deck class. Cards can be
added to and removed from hands. This gives two candidates for instance
methods in a Hand class. Cards are relatively passive things, but
we need to be able to determine their suits and values. We will discover
more instance methods as we go along.
First, we'll design the deck class in detail. When a deck of cards is first
created, it contains 52 cards in some standard order. The Deck class
will need a constructor to create a new deck. The constructor needs no
parameters because any new deck is the same as any other. There will be
an instance method called shuffle() that will rearrange the 52
cards into a random order. The dealCard() instance method will
get the next card from the deck. This will be a function with a return
type of Card, since the caller needs to know what card is being dealt.
It has no parameters. What will happen if there are no more cards in the
deck when its dealCard() method is called? It should probably be considered an error to try to deal a card from
an empty deck. But this raises another question: How will the rest of the
program know whether the deck is empty? Of course, the program could keep
track of how many cards it has used. But the deck itself should know how
many cards it has left, so the program should just be able to ask the deck
object. We can make this possible by specifying another instance method,
cardsLeft(), that returns the number of cards remaining in the deck.
This leads to a full specification of all the subroutines in the Deck
class:
Constructor and instance methods in class Deck:
public Deck()
// Constructor. Create an unshuffled deck of cards.
public void shuffle()
// Put all the used cards back into the deck,
// and shuffle it into a random order.
public int cardsLeft()
// As cards are dealt from the deck, the number of
// cards left decreases. This function returns the
// number of cards that are still left in the deck.
public Card dealCard()
// Deals one card from the deck and returns it.
This is everything you need to know in order to use the Deck class.
Of course, it doesn't tell us how to write the class. This has been an exercise
in design, not in programming. In fact, writing the class involves a
programming technique, arrays, which will not be covered until
Chapter 8. Nevertheless, you can look
at the source code, Deck.java, if you want. And given the source
code, you can use the class in your programs without understanding the
implementation.
We can do a similar analysis for the Hand class. When a hand object
is first created, it has no cards in it. An addCard() instance method
will add a card to the hand. This method needs a parameter of type Card
to specify which card is being added. For the removeCard() method,
a parameter is needed to specify which card to remove. But should we specify
the card itself ("Remove the ace of spades"), or should we specify
the card by its position in the hand ("Remove the third card in the hand")?
Actually, we don't have to decide, since we can allow for both options.
We'll have two removeCard() instance methods, one with a parameter of
type Card specifying the card to be removed and one with a parameter
of type int specifying the position of the card in the hand.
(Remember that you can have two methods in a class with the same name,
provided they have different types of parameters.) Since a hand can
contain a variable number of cards, it's convenient to be able to ask a
hand object how many cards it contains. So, we need an instance method
getCardCount() that returns the number of cards in the hand.
When I play cards, I like to arrange the cards in my hand so that cards
of the same value are next to each other. Since this is a generally useful
thing to be able to do, we can provide instance methods for sorting the cards
in the hand. Here is a full specification for a reusable Hand class:
Constructor and instance methods in class Hand:
public Hand() {
// Create a Hand object that is initially empty.
public void clear() {
// Discard all cards from the hand, making the hand empty.
public void addCard(Card c) {
// Add the card c to the hand. c should be non-null.
// (If c is null, nothing is added to the hand.)
public void removeCard(Card c) {
// If the specified card is in the hand, it is removed.
public void removeCard(int position) {
// If the specified position is a valid position in the
// hand, then the card in that position is removed.
public int getCardCount() {
// Return the number of cards in the hand.
public Card getCard(int position) {
// Get the card from the hand in given position, where
// positions are numbered starting from 0. If the
// specified position is not the position number of
// a card in the hand, then null is returned.
public void sortBySuit() {
// Sorts the cards in the hand so that cards of the same
// suit are grouped together, and within a suit the cards
// are sorted by value. Note that aces are considered
// to have the lowest value, 1.
public void sortByValue() {
// Sorts the cards in the hand so that cards of the same
// value are grouped together. Cards with the same value
// are sorted by suit. Note that aces are considered
// to have the lowest value, 1.
Again, you don't yet know enough to implement this class. But given the
source code, Hand.java, you can use the class in
your own programming projects.
We have covered enough material to write a Card
class. The class will have a constructor that specifies the value
and suit of the card that is being created. There are four suits, which
can be represented by the integers 0, 1, 2, and 3. It would be tough to
remember which number represents which suit, so I've defined named constants
in the Card class to represent the four possibilities. For
example, Card.SPADES is a constant that represents the suit, spades.
(These constants are declared to be public final
static ints. This is one case in which it makes sense to
have static members in a class that otherwise has only instance
variables and instance methods.) The possible values of a card are the
numbers 1, 2, ..., 13, with 1 standing for an ace, 11 for a jack, 12
for a queen, and 13 for a king. Again, I've defined some named constants
to represent the values of aces and face cards. So, cards can be
constructed by statements such as:
card1 = new Card( Card.ACE, Card.SPADES ); // Construct ace of spades.
card2 = new Card( 10, Card.DIAMONDS ); // Construct 10 of diamonds.
card3 = new Card( v, s ); // This is OK, as long as v and s
// are integer expressions.
A Card object needs instance variables to represent its value
and suit. I've made these private so that they cannot be changed
from outside the class, and I've provided instance methods getSuit()
and getValue() so that it will be possible to discover the suit
and value from outside the class. The instance variables are initialized
in the constructor, and are never changed after that. In fact, I've declared
the instance variables suit and value to be final,
since they are never changed after they are initialized. (An instance variable
can be declared final provided it is either given an initial value in its
declaration or is initialized in every constructor in the class.)
Finally, I've added a few convenience methods to the class to make it
easier to print out cards in a human-readable form. For example, I want
to be able to print out the suit of a card as the word "Diamonds",
rather than as the meaningless code number 2, which is used in the class to
represent diamonds. Since this is something that I'll probably have to do in
many programs, it makes sense to include support for it in the class.
So, I've provided instance methods getSuitAsString()
and getValueAsString() to return string representations of the
suit and value of a card. Finally, there is an instance method
toString() that returns a string with both the value and suit,
such as "Queen of Hearts". There is a good reason for calling
this method toString(). When any object is output with
System.out.print(), the object's toString() method is
called to produce the string representation of the object.
For example, if card refers to an object of type Card,
then System.out.println(card) is equivalent to
System.out.println(card.toString()). Similarly, if an
object is appended to a string
using the + operator, the object's toSring() method is used.
Thus,
System.out.println( "Your card is the " + card );
is equivalent to
System.out.println( "Your card is the " + card.toString() );
If the card is the queen of hearts, either of these will print out
"Your card is the Queen of Hearts".
Here is the complete Card class. It is general enough to be
highly reusable, so the work that went into designing, writing, and testing it
pays off handsomely in the long run.
/*
An object of class card represents one of the 52 cards in a
standard deck of playing cards. Each card has a suit and
a value.
*/
public class Card {
public final static int SPADES = 0, // Codes for the 4 suits.
HEARTS = 1,
DIAMONDS = 2,
CLUBS = 3;
public final static int ACE = 1, // Codes for non-numeric cards.
JACK = 11, // Cards 2 through 10 have
QUEEN = 12, // their numerical values
KING = 13; // for their codes.
private final int suit; // The suit of this card, one of the
// four constants: SPADES, HEARTS,
// DIAMONDS, CLUBS.
private final int value; // The value of this card, from 1 to 13.
public Card(int theValue, int theSuit) {
// Construct a card with the specified value and suit.
// Value must be between 1 and 13. Suit must be between
// 0 and 3. If the parameters are outside these ranges,
// the constructed card object will be invalid.
value = theValue;
suit = theSuit;
}
public int getSuit() {
// Return the int that codes for this card's suit.
return suit;
}
public int getValue() {
// Return the int that codes for this card's value.
return value;
}
public String getSuitAsString() {
// Return a String representing the card's suit.
// (If the card's suit is invalid, "??" is returned.)
switch ( suit ) {
case SPADES: return "Spades";
case HEARTS: return "Hearts";
case DIAMONDS: return "Diamonds";
case CLUBS: return "Clubs";
default: return "??";
}
}
public String getValueAsString() {
// Return a String representing the card's value.
// If the card's value is invalid, "??" is returned.
switch ( value ) {
case 1: return "Ace";
case 2: return "2";
case 3: return "3";
case 4: return "4";
case 5: return "5";
case 6: return "6";
case 7: return "7";
case 8: return "8";
case 9: return "9";
case 10: return "10";
case 11: return "Jack";
case 12: return "Queen";
case 13: return "King";
default: return "??";
}
}
public String toString() {
// Return a String representation of this card, such as
// "10 of Hearts" or "Queen of Spades".
return getValueAsString() + " of " + getSuitAsString();
}
} // end class Card
I will finish this section by presenting a complete program that uses
the Card and Deck classes. The program lets the
user play a very simple card game called HighLow. A deck of cards
is shuffled, and one card is dealt from the deck and shown to the user.
The user predicts whether the next card from the deck will be higher
or lower than the current card. If the user predicts correctly,
then the next card from the deck becomes the current card, and the
user makes another prediction. This continues until the user makes
an incorrect prediction. The number of correct predictions is
the user's score.
My program has a subroutine that plays one game of HighLow.
This subroutine has a return value that represents the user's score
in the game. The main() routine lets the user play several
games of HighLow. At the end, it reports the user's average score.
I won't go through the development of the algorithms used in this
program, but I encourage you to read it carefully and make sure that
you understand how it works. Here is the program:
/*
This program lets the user play HighLow, a simple card game
that is described in the output statements at the beginning of
the main() routine. After the user plays several games,
the user's average score is reported.
*/
public class HighLow {
public static void main(String[] args) {
TextIO.putln("This program lets you play the simple card game,");
TextIO.putln("HighLow. A card is dealt from a deck of cards.");
TextIO.putln("You have to predict whether the next card will be");
TextIO.putln("higher or lower. Your score in the game is the");
TextIO.putln("number of correct predictions you make before");
TextIO.putln("you guess wrong.");
TextIO.putln();
int gamesPlayed = 0; // Number of games user has played.
int sumOfScores = 0; // The sum of all the scores from
// all the games played.
double averageScore; // Average score, computed by dividing
// sumOfScores by gamesPlayed.
boolean playAgain; // Record user's response when user is
// asked whether he wants to play
// another game.
do {
int scoreThisGame; // Score for one game.
scoreThisGame = play(); // Play the game and get the score.
sumOfScores += scoreThisGame;
gamesPlayed++;
TextIO.put("Play again? ");
playAgain = TextIO.getlnBoolean();
} while (playAgain);
averageScore = ((double)sumOfScores) / gamesPlayed;
TextIO.putln();
TextIO.putln("You played " + gamesPlayed + " games.");
TextIO.putln("Your average score was " + averageScore);
} // end main()
static int play() {
// Lets the user play one game of HighLow, and returns the
// user's score in the game.
Deck deck = new Deck(); // Get a new deck of cards, and
// store a reference to it in
// the variable, Deck.
Card currentCard; // The current card, which the user sees.
Card nextCard; // The next card in the deck. The user tries
// to predict whether this is higher or lower
// than the current card.
int correctGuesses ; // The number of correct predictions the
// user has made. At the end of the game,
// this will be the user's score.
char guess; // The user's guess. 'H' if the user predicts that
// the next card will be higher, 'L' if the user
// predicts that it will be lower.
deck.shuffle();
correctGuesses = 0;
currentCard = deck.dealCard();
TextIO.putln("The first card is the " + currentCard);
while (true) { // Loop ends when user's prediction is wrong.
/* Get the user's prediction, 'H' or 'L'. */
TextIO.put("Will the next card be higher (H) or lower (L)? ");
do {
guess = TextIO.getlnChar();
guess = Character.toUpperCase(guess);
if (guess != 'H' && guess != 'L')
TextIO.put("Please respond with H or L: ");
} while (guess != 'H' && guess != 'L');
/* Get the next card and show it to the user. */
nextCard = deck.dealCard();
TextIO.putln("The next card is " + nextCard);
/* Check the user's prediction. */
if (nextCard.getValue() == currentCard.getValue()) {
TextIO.putln("The value is the same as the previous card.");
TextIO.putln("You lose on ties. Sorry!");
break; // End the game.
}
else if (nextCard.getValue() > currentCard.getValue()) {
if (guess == 'H') {
TextIO.putln("Your prediction was correct.");
correctGuesses++;
}
else {
TextIO.putln("Your prediction was incorrect.");
break; // End the game.
}
}
else { // nextCard is lower
if (guess == 'L') {
TextIO.putln("Your prediction was correct.");
correctGuesses++;
}
else {
TextIO.putln("Your prediction was incorrect.");
break; // End the game.
}
}
/* To set up for the next iteration of the loop, the nextCard
becomes the currentCard, since the currentCard has to be
the card that the user sees, and the nextCard will be
set to the next card in the deck after the user makes
his prediction. */
currentCard = nextCard;
TextIO.putln();
TextIO.putln("The card is " + currentCard);
} // end of while loop
TextIO.putln();
TextIO.putln("The game is over.");
TextIO.putln("You made " + correctGuesses
+ " correct predictions.");
TextIO.putln();
return correctGuesses;
} // end play()
} // end class HighLow
Here is an applet that simulates the program: