Section 5.5
More Details of Classes
ALTHOUGH THE BASIC IDEAS of object-oriented programming
are reasonably simple and clear, they are subtle, and they take time to get used to.
And unfortunately, beyond the basic ideas there are a lot of details. This section
covers more of those annoying details. You should not necessarily master everything
in this section the first time through, but you should read it to be aware of
what is possible. (This doesn't apply to the first subsection, below, which
you definitely need to master.) For the most part, when I need to use material from
this section later in the text, I will explain it again briefly, or I will refer you
back to this section.
Extending Existing Classes
The previous section discussed subclasses, including information
about how to program with subclasses in Java. However, that section dealt mostly
with the theory. In this section, I want to emphasize the practical matter of
Java syntax by giving an example.
In day-to-day programming, especially for programmers who are just beginning
to work with objects, subclassing is used mainly in one situation. There is
an existing class that can be adapted with a few changes or additions. This is
much more common than designing groups of classes and subclasses from scratch.
The existing class can be extended to make
a subclass. The syntax for this is
class subclass-name extends existing-class-name {
.
. // Changes and additions.
.
}
(Of course, the class can optionally be declared to be public.)
As an example, suppose you want to write a program that plays the card
game, Blackjack. You can use the Card, Hand, and Deck
classes developed in Section 3. However, a hand in the
game of Blackjack is a little different from a hand of cards in general,
since it must be possible to compute the "value" of a Blackjack hand
according to the rules of the game. The rules are as follows: The value of
a hand is obtained by adding up the values of the cards in the hand. The value
of a numeric card such as a three or a ten is its numerical value.
The value of a Jack, Queen, or King is 10. The value of an Ace can be either
1 or 11. An Ace should be counted as 11 unless doing so would put the total
value of the hand over 21. (Note that this means that the second, third, or
fourth Ace in the had will always be counted as 1.)
One way to handle this is to extend the existing Hand class
by adding a method that computes the Blackjack value of the hand. Here's
the definition of such a class:
public class BlackjackHand extends Hand {
public int getBlackjackValue() {
// Returns the value of this hand for the
// game of Blackjack.
int val; // The value computed for the hand.
boolean ace; // This will be set to true if the
// hand contains an ace.
int cards; // Number of cards in the hand.
val = 0;
ace = false;
cards = getCardCount();
for ( int i = 0; i < cards; i++ ) {
// Add the value of the i-th card in the hand.
Card card; // The i-th card;
int cardVal; // The blackjack value of the i-th card.
card = getCard(i);
cardVal = card.getValue(); // The normal value, 1 to 13.
if (cardVal > 10) {
cardVal = 10; // For a Jack, Queen, or King.
}
if (cardVal == 1) {
ace = true; // There is at least one ace.
}
val = val + cardVal;
}
// Now, val is the value of the hand, counting any ace as 1.
// If there is an ace, and if changing its value from 1 to
// 11 would leave the score less than or equal to 21,
// then do so by adding the extra 10 points to val.
if ( ace == true && val + 10 <= 21 )
val = val + 10;
return val;
} // end getBlackjackValue()
} // end class BlackjackHand
Since BlackjackHand is a subclass of Hand, an object of
type BlackjackHand contains all the instance variables and instance methods
defined in Hand, plus the new instance method getBlackjackValue().
For example, if bHand is a variable of type BlackjackHand,
then the following are all legal method calls:
bHand.getCardCount(), bHand.removeCard(0), and
bHand.getBlackjackValue().
Inherited variables and methods from the Hand class can also be used
in the definition of BlackjackHand (except for any that are declared
to be private). The statement "cards = getCardCount();"
in the above definition of getBlackjackValue() calls the instance method
getCardCount(), which was defined in the Hand class.
Extending existing classes is an easy way to build on previous work.
We'll see that many standard classes have been written specifically to
be used as the basis for making subclasses.
Interfaces
Some object-oriented programming languages, such as C++, allow
a class to extend two or more superclasses. This is
called multiple inheritance.
In the illustration below, for example, class
E is shown as having both class A and class B as direct
superclasses, while class F has three direct superclasses.
Such multiple inheritance is not allowed
in Java. The designers of Java wanted to keep the language
reasonably simple, and felt that the benefits of
multiple inheritance were not worth the cost in
increased complexity. However, Java does have a feature
that can be used to accomplish many of the same goals
as multiple inheritance: interfaces.
We've encountered the term "interface" before, in
connection with black boxes in general and subroutines in particular.
The interface of a subroutine consists of the name of the subroutine,
its return type, and the number and types of its parameters. This
is the information you need to know if you want to call the subroutine.
A subroutine also has an implementation: the block of code which
defines it and which is executed when the subroutine is called.
In Java, interface is a reserved word with an additional
meaning. An "interface" in Java consists of a set of subroutine interfaces, without
any associated implementations. A class can implement
an interface by providing an implementation for each of the
subroutines specified by the interface. Here is an example of a very simple
Java interface:
public interface Drawable {
public void draw();
}
This looks much like a class definition, except that the implementation
of the method draw() is omitted. A class that implements
the interface, Drawable, must provide an implementation
for this method. Of course, the class can also include other methods and variables.
For example,
class Line implements Drawable {
public void draw() {
. . . // do something -- presumably, draw a line
}
. . . // other methods and variables
}
While a class can extend only one other class, it can implement
any number of interfaces. In fact, a class can both extend another
class and implement one or more interfaces. So, we can have
things like
class FilledCircle extends Circle
implements Drawable, Fillable {
. . .
}
The point of all this is that, although interfaces are not classes,
they are something very similar.
An interface is very much like an abstract class, that is,
a class that can never be used for constructing objects,
but can be used as a basis for building other classes.
The subroutines in an interface are abstract methods,
which must be implemented in any concrete class that implements
the interface. And as with abstract classes, even though you
can't construct an object from an interface, you can declare a
variable whose type is given by the interface. For example,
if Drawable is an interface, and if Line
and FilledCircle are classes that implement Drawable,
then you could say:
Drawable figure; // Declare a variable of type Drawable. It can
// refer to any object that implements the
// Drawable interface.
figure = new Line(); // figure now refers to an object of class Line
figure.draw(); // calls draw() method from class Line
figure = new FilledCircle(); // Now, figure refers to an object
// of class FilledCircle.
figure.draw(); // calls draw() method from class FilledCircle
A variable of type Drawable can refer to any object
of any class that implements the Drawable interface.
A statement like figure.draw(), above, is legal because
any such class has a draw() method.
Note that a type is something
that can be used to declare variables. A type can also be used
to specify the type of a parameter in a subroutine, or the return
type of a function. In Java, a type can
be either a class, an interface, or one of the eight built-in
primitive types. These are the only possibilities.
Of these, however, only classes can be used to
construct new objects.
You are not likely to need to write your own interfaces until
you get to the point of writing fairly complex programs. However,
there are a few interfaces that are used in important ways in
Java's standard packages. You'll learn about some of these
standard interfaces in the next few chapters.
The Special Variables this and super
A static member of a class has a simple name, which can only be used inside the
class definition. For use outside the class, it has a full name
of the form class-name.simple-name.
For example, "System.out" is a static member variable with
simple name "out" in the class "System".
It's always legal to use the full name of a static member, even within the
class where it's defined. Sometimes it's even necessary, as when the simple name
of a static member variable is hidden by a local variable of the same name.
Instance variables and instance methods also have simple names that can
be used inside the class where the variable or method is defined. But a class
does not actually contain instance variables or methods, only their source code.
Actual instance variables and methods are contained in objects. To get at
an instance variable or method from outside the class definition, you need
a variable that refers to the object. Then the full name is of the
form variable-name.simple-name.
But suppose you are writing a class definition, and you want to refer to the
object that contains the instance method you are writing? Suppose you want to
use a full name for an instance variable, because its simple name is hidden
by a local variable?
Java provides a special, predefined variable named
"this" that you can use for these purposes. The variable,
this, can be used in the source code of an instance method to refer
to the object that contains the method. If x is an instance variable,
then this.x can be used as a full name for that variable.
Whenever the computer executes an instance method, it sets the variable,
this, to refer to the object that contains the method.
One common use of this is in constructors. For example:
public class Student {
private String name; // Name of the student.
public Student(String name) {
// Constructor. Create a student with specified name.
this.name = name;
}
.
. // More variables and methods.
.
}
In the constructor, the instance variable called name is hidden by
a formal parameter. However, the instance variable can still be referred to
by its full name, this.name. In the assignment statement, the
value of the formal parameter, name, is assigned to the instance
variable, this.name. This is considered to be acceptable style:
There is no need to dream up cute new names for formal parameters that are just
used to initialize instance variables. You can use the same name for the
parameter as for the instance variable.
There is another common use for this. Sometimes, when you are writing
an instance method, you need to pass the object that contains the method
to a subroutine, as an actual parameter. In that case, you can use
this as the actual parameter. For example, if you wanted
to print out a string representation of the object, you could say
"System.out.println(this);".
Java also defines another special variable, named "super",
for use in the definitions of instance methods. The variable super is for
use in a subclass. Like this, super
refers to the object that contains the method. But it's forgetful.
It forgets that the object belongs to the class you are writing,
and it remembers only that it belongs to the superclass of that
class. The point is that the class can contain additions and
modifications to the superclass. super doesn't know
about any of those additions and modifications. Let's say that
the class that you are writing contains an instance method
named doSomething(). Consider the subroutine
call statement super.doSomething(). Now, super
doesn't know anything about the doSomething() method
in the subclass. It only knows about things in the superclass,
so it tries to execute a method named doSomething()
from the superclass. If there is none -- if the doSomething()
method was an addition rather than a modification -- you'll get a syntax
error.
The reason super exists is so you can get access to
things in the superclass that are hidden
by things in the subclass. For example, super.x always refers to an instance
variable named x in the superclass. This can be useful for the following reason:
If a class contains an instance variable with the same name
as an instance variable in its superclass, then an object
of that class will actually contain two variables with the same name:
one defined as part of the class itself and one defined as part of
the superclass. The variable in the subclass does not replace
the variable of the same name in the superclass; it merely
hides it. The variable from the superclass can still be
accessed, using super.
When you write a method in a subclass that has the
same signature as a method in its superclass, the method from
the superclass is hidden in the same way. We say that
the method in the subclass overrides
the method from the superclass. Again, however, super
can be used to access the method from the superclass.
The major use of super is to override a method
with a new method that extends the behavior of the inherited
method, instead of replacing that behavior entirely. The new
method can use super to call the method from the superclass,
and then it can add additional code to provide additional
behavior. As an example, suppose you have a PairOfDice
class that includes a roll() method. Suppose that
you want a subclass, GraphicalDice, to represent a
pair of dice drawn on the computer screen. The roll()
method in the GraphicalDice method should change
the values of the dice and redraw the dice to show the new values.
The GraphicalDice class might look something like this:
public class GraphicalDice extends PairOfDice {
public void roll() {
// Roll the dice, and redraw them.
super.roll(); // Call the roll method from PairOfDice.
redraw(); // Call a method to draw the dice.
}
.
. // More stuff, including definition of redraw().
.
}
Note that this allows you to extend the behavior of the roll()
method even if you don't know how the method is implemented in the
superclass!
Here is a more complete example. The applet at the end
of Section 4.7 shows a disturbance that moves around in a mosaic of
little squares. As it moves, the squares it visits become a brighter
red. The result looks interesting, but I think it would be prettier
if the pattern were symmetric. A symmetric version of the applet
is shown at the bottom of this page. The symmetric applet can be
programmed as an easy extension of the original applet.
In the symmetric version, each time a square is brightened,
the squares that can be obtained from that one by horizontal and vertical reflection
through the center of the mosaic are also brightened.
The four red squares in the picture, for example, form
a set of such symmetrically placed squares, as do the
purple squares and the green squares. (The blue square is
at the center of the mosaic, so reflecting it doesn't produce
any other squares; it's its own reflection.)
The original applet is defined by the class RandomBrighten.
This class uses features of Java that you won't learn about for a while yet,
but the actual task of brightening a square is done by a single
method called brighten(). If row and col
are the row and column numbers of a square, then "brighten(row,col);"
increases the brightness of that square. All we need is a subclass
of RandomBrighten with a modified brighten() routine.
Instead of just brightening one square, the modified routine will
also brighten the horizontal and vertical reflections of that square.
But how will it brighten each of the four individual squares?
By calling the brighten() method from the original
class. It can do this by calling super.brighten().
There is still the problem of computing the row and column numbers
of the horizontal and vertical reflections. To do this, you need
to know the number of rows and the number of columns. The
RandomBrighten class has instance variables named
ROWS and COLUMNS to represent these quantities.
Using these variables, it's possible to come up with formulas
for the reflections, as shown in the definition of the brighten()
method below.
Here's the complete definition of the new class:
public class SymmetricBrighten extends RandomBrighten {
void brighten(int row, int col) {
// Brighten the specified square and its horizontal
// and vertical reflections. This overrides the brighten
// method from the RandomBrighten class, which just
// brightens one square.
super.brighten(row, col);
super.brighten(ROWS - 1 - row, col);
super.brighten(row, COLUMNS - 1 - col);
super.brighten(ROWS - 1 - row, COLUMNS - 1 - col);
}
} // end class SymmetricBrighten
This is the entire source code for the applet at the bottom
of this page.
Constructors in Subclasses
Constructors are not inherited. That is, if you extend an existing class
to make a subclass, the constructors in the superclass do not become
part of the subclass. If you want constructors in the subclass, you have
to define new ones from scratch. If you don't define any constructors
in the subclass, then the computer will make up a default constructor,
with no parameters, for you.
This could be a problem, if there is a constructor in the superclass
that does a lot of necessary work. It looks like you might have to repeat
all that work in the subclass! This could be a real problem
if you don't have the source code to the superclass, and don't know how
it works, or if the constructor in the superclass initializes private
member variables that you don't even have access to in the subclass!
Obviously, there has to be some fix for this, and there is. It involves
the special variable, super, which was introduced in the
previous subsection. As the very first statement in a constructor, you
can use super to call a constructor from the superclass. The notation
for this is a bit ugly and misleading, and it can only be used in this one
particular circumstance: It looks like you are calling super as
a subroutine (even though super is not a subroutine and you can't
call constructors the same way you call other subroutines anyway). As an example,
assume that the PairOfDice class has a constructor
that takes two integers as parameters. Consider a subclass:
public class GraphicalDice extends PairOfDice {
public GraphicalDice() { // Constructor for this class.
super(3,4); // Call the constructor from the
// PairOfDice class, with parameters 3, 4.
initializeGraphics(); // Do some initialization specific
// to the GraphicalDice class.
}
.
. // More constructors, methods, variables...
.
}
This might seem rather technical, but unfortunately it is sometimes
necessary. By the way, you can use the special variable this
in exactly the same way to call another constructor in the same class.
This can be useful since it can save you from repeating the same code
in several constructors.
More about Access Modifiers
A class can be declared to be public. A public class can be
accessed from anywhere. Certain classes have to be public. A class that
defines a stand-alone application must be public, so that the system will
be able to get at its main() routine. A class that defines
an applet must be public so that it can be used by a Web browser. If
a class is not declared to be public, then it can only be used
by other classes in the same "package" as the class.
Packages are discussed in Section 4.5.
Classes that are not explicitly declared to be in any package
are put into something called the default package. All the examples
in this textbook are in the default package, so they are all
accessible to one another whether or not they are declared public.
So, except for applications and applets, which must be public,
it makes no practical difference whether our classes are declared
to be public or not.
However, once you start writing packages, it does make a difference.
A package should contain a set of related classes. Some of those classes
are meant to be public, for access from outside the package. Others
can be part of the internal workings of the package, and they should
not be made public. A package is a kind of black box. The public
classes in the package are the interface. (More exactly, the public
variables and subroutines in the public classes are the interface).
The non-public classes are part of the non-public implementation.
Of course, all the classes in the package have unrestricted access
to one another.
Following this model, I will tend to declare a class public
if it seems like it might have some general applicability.
If it is written just to play some sort of auxiliary role
in a larger project, I am more likely not to make it
public.
A member variable or subroutine in a class can also be declared to be
public, which means that it is accessible from anywhere.
It can be declared to be private, which means that it
accessible only from inside the class where it is defined.
Making a variable private gives you complete control over
that variable. The only code that will ever manipulate it
is the code you write in your class. This is an important
kind of protection.
If no access modifier is specified for a variable or subroutine,
then it is accessible from any class in the same package as the class.
As with classes, in this textbook there is no practical difference between
declaring a member public and using no access modifier at all.
However, there might be stylistic reasons for
preferring one over the other. And a real difference does arise once
you start writing your own packages.
There is a third access modifier that can be applied to a member
variable or subroutine. If it is declared to be protected,
then it can be used in the class where it is defined and in any
subclass of that class. This is obviously less restrictive than
private and more restrictive than public.
Classes that are written specifically to be used as a basis
for making subclasses often have protected members.
The protected members are there to provide a
foundation for the subclasses to build on. But they are still
invisible to the public at large.
Mixing Static and Non-static
Classes, as I've said, have two very distinct purposes. A class can be used
to group together a set of static member variables and static member
subroutines. Or it can be used as a factory for making objects.
The non-static variables and subroutine definintions in the class
specify the instance variables and methods of the objects. In most cases,
a class performs one or the other of these roles, not both.
Sometimes, however, static and non-static members are mixed in a single
class. In this case, the class plays a dual role. Sometimes, these
roles are completely separate. It is also possible for the static and
non-static parts of a class to interact. This happens when instance
methods use static member variables or call static member subroutines.
An instance method belongs to an object, not to the class itself, and there
can be many objects with their own versions of the instance method.
But there is only one copy of a static member variable. So, effectively,
we have many objects sharing that one variable.
As an example, let's rewrite the Student class that
was used in the Section 2. I've added
an ID for each student and a static member
called nextUniqueID. Although there is an
ID variable in each student object, there is
only one nextUniqueID variable.
public class Student {
private String name; // Student's name.
private int ID; // Unique ID number for this student.
public double test1, test2, test3; // Grades on three tests.
private static int nextUniqueID = 0;
// keep track of next available unique ID number
Student(String theName) {
// Constructor for Student objects;
// provides a name for the Student,
// and assigns the student a unique
// ID number.
name = theName;
nextUniqueID++;
ID = nextUniqueID;
}
public String getName() {
// Accessor method for reading value of private
// instance variable, name.
return name;
}
public int getID() {
// Accessor method for reading value of ID.
return ID;
}
public double getAverage() {
// Compute average test grade.
return (test1 + test2 + test3) / 3;
}
} // end of class Student
The initialization "nextUniqueID = 0" is done only once,
when the class is first loaded. Whenever a Student object is constructed
and the constructor says "nextUniqueID++;", it's always the
same static member variable that is being incremented. When the very first Student
object is created, nextUniqueID becomes 1. When the second object is
created, nextUniqueID becomes 2. After the third object, it becomes 3.
And so on. The constructor stores the new value of nextUniqueID in
the ID variable of the object that is being created. Of course,
ID is an instance variable, so every object has its own individual
ID variable. The class is constructed so that each student will
automatically get a different value for its ID variable. Furthermore,
the ID variable is private, so there is no way for this
variable to be tampered with after the object has been created.
You are guaranteed, just by the way the class is designed, that every
student object will have its own permanent, unique identification number.
Which is kind of cool if you think about it.
End of Chapter 5