Over the many years
since the dawn of computing, people have studied software-development
approaches to figure out which approaches are quickest, cheapest, most
reliable, and produce the best software. And over the years, many approaches
and technologies have reigned as the best. As time goes on, we learn more about
software development and are able to modify and adapt our approaches based on
what we learn. The type of software we develop also changes over time as a result
of improvements in computer hardware, innovations in computer science, and
changes in user expectations. These improvements affect our development
approaches as well.
Of all the known
approaches to developing software, one approach, called the object-oriented
approach, has repeatedly proven itself to be the best approach for a large
class of common software applications. It's likely that the object-oriented
approach will undergo further evolution and that a new, improved
software-development paradigm will take its place. But for right now, and the
foreseeable future, it is recognized as the best approach for the majority of
software that we develop today.
Object-oriented
programming focuses on the development of self-contained software components,
called objects. These objects are modeled after things, such as files,
forms, buttons, and windows, that appear in the real world. Objects are defined
in terms of the information they contain and the operations they provide for
using and manipulating this information.
This book is an
object. It contains a lot of information. (If you don't believe me, try
retyping it.) It also has methods for accessing the information it contains.
For example, you can open the book, turn a page, read a paragraph, search the
table of contents, and so on. The information contained in the book, together
with the methods for accessing it, are what comprise the object known as this
book.
In order to read this
book, you need some sort of light source. You could be reading it in the open
sunshine or by moonlight, but let's assume that you are using a lamp of some
kind. A lamp is also an object. It is an example of an object that contains
information about its state. The state of an object is the particular condition
it is in. For example, a lamp can be on or off. The lamp's methods-turn lamp on
and turn lamp off-are used to access the state of the lamp.
This book, too, has
state information. For example, it can be open or closed. If it is open, it can
be opened to a particular page. The pages are objects in their own right. They
contain information and can be accessed through the read page method. The book
object can be viewed as being composed of page objects. The book's methods
provide access to pages, and the page methods provide access to the information
contained on a particular page.
The information
contained in an object, whether it is state specific or not, is referred to as
the object's data. The object's methods are said to access the
data. Some methods return information about the object's data and are said to
support read access. Other methods cause the data to be modified and are
said to provide write access to the data. Finally, as you'll learn in
later sections, some methods, called constructors, are used to create
objects.
The fact that one
object can be composed of, or built from, other objects is the heart of
object-oriented programming. This allows more complex objects to be constructed
from simple object components. Just as you would not write a book as one
continuous stream of text (unless you are Jack Kerouac), you wouldn't write a
program as a single sequence of source code instructions. You design your
program as an application object and construct it from other objects that are
built or borrowed.
For example, suppose
you are developing a drawing program. Your drawing application would consist of
objects such as windows, menus, a drawing canvas, a tool palette, a color
palette, and so on. Some of these objects would be available in object
libraries and others would be built from more primitive components. You would
develop your drawing application by gathering and building its component
objects and assembling them into an integrated whole.
Object composition not
only allows you to simplify the organization of your programs, it also lets you
reuse the software you develop. For example, you could develop drawing objects
as part of your drawing program and then reuse those objects in a paint program
and a desktop- publishing program. You could also package up your drawing
objects and give or sell them to others so that they can use them as a
foundation for building their own custom objects.
Object reuse provides
you with the capability to build or acquire a library of objects from which you
can more quickly and easily piece together your programs. Without this
capability, you are forced to start from scratch with every program that you
develop.
Object reuse is not
limited to object composition. It also exploits a powerful capability of
object-oriented programming known as inheritance. Inheritance not only
allows objects to be used as is, but also allows new objects to be created by
extending and tailoring existing objects. Before you learn about inheritance,
however, the concept of an object's class must be explained.
At this point, you
might be wondering just how you go about developing objects. The answer, of
course, depends on the language you are using. Java, C++, Smalltalk, and some
other object-oriented languages follow a class-based approach. This approach
allows you to declare classes that serve as a template from which
objects are created.
As you would expect, a
class defines the type of data that is contained in an object and the
methods that are used to access this data. A class also defines one or more
methods to be used to create objects that are instances of the class. An
instance of a class is a concrete manifestation of the class in your computer's
memory.
For example, consider
a job application form as an object. It contains data-the different form fields
that must be filled out. There are also methods for accessing the data-for
example, fill in form and read form. Now suppose that you develop an
application form for a company that will use it for new job applicants. When a
job is advertised, 100 potential applicants show up. In order for these
applicants to use your form, they must all be given a unique instance of the
form. These form instances are created by using the form you developed as a master
copy and then duplicating the master copy as many times as needed to create
each instance. The job applicants then fill in their instances of the form,
using the fill in form method.
In the preceding
example, the master form is analogous to a class. The master form defines the
data to be contained in each of its instances and implicitly provides methods
by which the data can be accessed. In the same way, a class defines the data
that can be contained in an object as well as the methods that can be used to
access this data.
Classification is a
common way that we organize knowledge. When we encounter a new object in our
daily experience, we try to fit that object in our hierarchical classification
scheme. If it fits in an existing category, we know what kind of object it is.
If it doesn't fit, we add a new category. Figure 5.1
describes how we use classification to represent knowledge.
Figure 5.1. Hierarchical
classification of knowledge.
Figure 5.1 : Hierarchical classification of
knowledge.
When we classify
objects in this hierarchical fashion, the object categories at the top of the
classification tree include all the object categories below them. If an object
category appears in the classification tree, it satisfies the properties of all
object categories above it in the tree. Figure 5.2 presents
a classification tree for vehicles. All categories in the tree below the
category automobile, for example, share the common characteristics of being
four-wheeled, self-powered, and designed for passenger transportation.
Figure 5.2 : Vehicle classification tree.
The fact that a
lower-level category shares the characteristics of the categories above it on
the classification tree is known as inheritance. The lower-level
categories are said to inherit the characteristics of the categories above them
on the tree.
At this point, you're
probably wondering what any of this has to do with object-oriented programming
in general, and Java software development in particular. We're almost there.
The classes you
learned about in the previous section can also be organized in a hierarchical
fashion. A class X is said to extend another class Y if it contains all
the data contained in class Y and implements all the methods implemented by
class Y. Class X is said to be a subclass of class Y, and class Y is
said to be a superclass, or parent class, of class X.
Classes form a
hierarchical classification tree under the subclass relationship. If a class X
is a subclass of a class Y, it inherits the properties of Y. This means that
all of the data and methods defined for class Y are available to class X.
Most object-oriented
programming languages, and Java in particular, allow you to easily define
subclasses that automatically inherit the data and methods of the classes they
extend. This is a very powerful feature for software reuse. Not only can you
reuse classes as they are defined, but you can easily extend and tailor their
definitions by adding additional data and access methods to their subclasses.
There are many times
that you may have a class definition you can use in your program, but it would
be better if it supported additional state information or access methods.
Java's support of subclassing enables you to easily extend such classes by
supplying only the additional data and methods that are unique to the subclass.
This allows you to take advantage of all the features of the superclass without
having to implement any of them.
When a class extends
another class, it inherits the data and methods of the class it extends. This
is known as single inheritance. It is also possible for a class to
extend classes on more than one branch of the class hierarchy tree, as shown in
Figure 5.3. This is known as multiple inheritance.
Figure 5.3 : Multiple inheritance.
Multiple inheritance
poses some difficulties for object-oriented programming. Most of these
difficulties stem from the problem of determining which parent of a class to
use under certain conditions. Numerous ambiguities arise when a class may have
more than one immediate parent. For example, suppose a class X extends both a
class Y and a class Z. Both class Y and class Z implement a unique print
method. How does the compiler determine what method should be used to print
objects of class X? What if the ambiguity arises during runtime for an object
that inherits methods over several widely spaced branches of the class hierarchy?
What's a poor compiler to do?
It is possible to
design compilers and runtime systems that solve the ambiguities resulting from
multiple inheritance, but these solutions tend to introduce a significant
amount of processing overhead, adversely affecting program size and
performance. The developers of Java have opted to support only single
inheritance. This greatly simplifies the Java language, compiler, and runtime
system. Java uses the interface construct to provide the benefits of multiple
inheritance without the drawbacks resulting from parent ambiguity. You'll learn
more about this construct in Chapter 6,
"Interfaces."
In a pure object-oriented
programming model, such as that used by Smalltalk, objects interact by sending
messages to each other. When an object receives a message, the object invokes a
method to process the message. The method may change the state of the object,
return information contained in the object, or cause objects to be created or
deleted.
The object model used
by Java is consistent with the concept of message passing, but does not
emphasize it. In the Java model, objects interact by invoking each other's
methods. Methods provide access to the information contained in an
object. The type of access varies depending on the method.
One of the
characteristics of object-oriented programming that is often touted in
discussions of the subject is encapsulation. The term carries the
connotation of an object being enclosed in some sort of container-and that is
exactly what it means. Encapsulation is the combining of data and the code that
manipulates that data into a single component-that is, an object. Encapsulation
also refers to the control of access to the details of an object's
implementation. Object access is limited to a well-defined, controlled
interface. This allows objects to be self-contained and protects them from
accidental misuse, both of which are important to reliable software design.
Polymorphism
is the ability to assume different forms. In object-oriented programming, this
refers to the ability of objects to have many methods of the same name, but
with different forms. The compiler and runtime system support polymorphism by
matching each method invocation to the correct method, class, and object. The
ability to figure out which method to use, in complex situations, is the
essence of polymorphism.
Luckily for us,
polymorphism is implemented in the compiler and runtime system-we don't need to
do anything to make it happen. We just need to know that it works.
Sometimes a program
might need to interface with objects of many different classes. For example,
consider a program that has the responsibility of sending out objects over a
communication link. The program may not know what class an object belongs to
until it is time to send it. The capability to defer until runtime decisions
about what class an object belongs to and the methods for accessing the object
is known as dynamic binding.
Dynamic binding is
important to object-oriented programming because it eliminates many potentially
constraining assumptions about the classes that an object belongs to and
enables objects to be designed in a more general and open manner.
Dynamic binding also
provides capabilities that are necessary for the advanced network programming
capabilities of Java applets. When a browser executes a Java applet, the applet
could require the loading of classes located on other sites across the
Internet. Furthermore, these classes could be in a continual state of
modification and upgrade. Dynamic binding allows new and modified objects to be
used by executing software without requiring recompilation. The compiler and
interpreter work together to provide executable code with the capabilities
needed to dynamically interface with unknown objects during program execution.
So far, you’ve managed
to avoid the issue of object-oriented programming and how it relates to Java.
This chapter aims to remedy that problem. It begins with a basic discussion of
object-oriented programming in general. With this background in place, you can
then move into the rest of the chapter, which covers the specific elements of
the Java language that provide support for object-oriented programming—namely,
classes, packages, and interfaces.
You can think of this
chapter as the chapter that finishes helping you to your feet in regard to
learning the Java language. Classes are the final core component of the Java
language that you need to learn to be a proficient Java programmer. Once you
have a solid understanding of classes and how they work in Java, you’ll be
ready to write some serious Java programs. So, what are you waiting for, read
on!
If you’ve been
anywhere near the computer section of a bookstore or picked up a programming
magazine in the last five years, you’ve no doubt seen the hype surrounding
object-oriented programming. It’s the most popular, yet generally least
understood programming technology to come about in a while, and it all revolves
around the concept of an object.
You may have been
wondering what the big deal is with objects and object-oriented technology. Is
it something you should be concerned with, and if so, why? If you sift through
the hype surrounding the whole object-oriented issue, you’ll find a very
powerful technology that provides a lot of benefits to software design. The
problem is that object-oriented concepts can be difficult to grasp. And you
can’t embrace the benefits of object-oriented design if you don’t completely
understand what they are. Because of this, a complete understanding of the
theory behind object-oriented programming is usually developed over time
through practice.
A lot of the confusion
among developers in regard to object-oriented technology has led to confusion
among computer users in general. How many products have you seen that claim
they are object-oriented? Now, considering the fact that object-orientation is
a software design issue, what can this statement possibly mean to a software
consumer? In many ways, “object-oriented” has become to the software industry
what “new and improved” is to the household cleanser industry. The truth is
that the real world is already object-oriented, which is no surprise to anyone.
The significance of object-oriented technology is that it enables programmers
to design software in much the same way that they perceive the real world.
Now that you’ve come
to terms with some of the misconceptions surrounding the object-oriented issue,
try to put them aside and think of what the term object-oriented might mean to
software design. This primer lays the groundwork for understanding how object-oriented
design makes writing programs faster, easier, and more reliable. And it all
begins with the object. Even though this chapter ultimately focuses on Java,
this object-oriented primer section really applies to all object-oriented
languages.
Objects are software
bundles of data and the procedures that act on that data. The procedures are
also known as methods. The merger of data and methods provides a means of more
accurately representing real-world objects in software. Without objects, modeling
a real-world problem in software requires a significant logical leap. Objects,
on the other hand, enable programmers to solve real-world problems in the
software domain much easier and more logically.
As evident by its
name, objects are at the heart of object-oriented technology. To understand how
software objects are beneficial, think about the common characteristics of all
real-world objects. Lions, cars, and calculators all share two common
characteristics: state and behavior. For example, the state of a lion might
include color, weight, and whether the lion is tired or hungry. Lions also have
certain behaviors, such as roaring, sleeping, and hunting. The state of a car
includes the current speed, the type of transmission, whether it is two- or
four-wheel drive, whether the lights are on, and the current gear, among other
things. The behaviors for a car include turning, braking, and accelerating.
As with real-world
objects, software objects also have these two common characteristics (state and
behavior). To relate this back to programming terms, the state of an object is
determined by its data and the behavior of an object is defined by its methods.
By making this connection between real-world objects an-d software objects, you
begin to see how objects help bridge the gap between the real world and the
world of software inside your computer.
Because software
objects are modeled after real-world objects, you can more easily represent
real-world objects in object-oriented programs. You could use the lion object
to represent a real lion in an interactive software zoo. Similarly, car objects
would turn out very useful in a racing game. However, you don’t always have to
think of software objects as modeling physical real-world objects; software
objects can be just as useful for modeling abstract concepts. For example, a
thread is an object used in multithreaded software systems that represents a
stream of program execution. You’ll learn a lot more about threads and how they
are used in Java in the next chapter, “Threads and Multithreading.”
Figure 14.1 shows a
visualization of a software object, including the primary components and how
they relate.
A software project.
The software object in
Figure 14.1 clearly shows the two primary components of an object: data and
methods. The figure also shows some type of communication, or access, between
the data and the methods. Additionally, it shows how messages are sent through
the methods, which result in responses from the object. You’ll learn more about
messages and responses a little later in this chapter.
The data and methods
within an object express everything that the object represents (state), along
with what all it can do (behavior). A software object modeling a real-world car
would have variables (data) that indicate the car’s current state: It’s
traveling at 75 mph, it’s in 4th gear, and the lights are on. The software car
object would also have methods that allow it to brake, accelerate, steer,
change gears, and turn the lights on and off. Figure 14.2 shows what a software
car object might look like.
A software car
project.
In both Figures 14.1
and 14.2 you probably noticed the line separating the methods from the data
within the object. This line is a little misleading because methods have full
access tothe data within an object. The line is there to illustrate the
difference between the visibility of the methods and the data to the outside.
In this sense, an object’s visibility refers to what parts of the object
another object has access. Because object data defaults to being invisible, or
inaccessible to other objects, all interaction between objects must be handled
through methods. This hiding of data within an object is called encapsulation.
Encapsulation is the
process of packaging an object’s data together with its methods. A powerful
benefit of encapsulation is the hiding of implementation details from other
objects. This means that the internal portion of an object has more limited
visibility than the external portion. This results in a safeguarding of the
internal portion against unwanted external access.
The external portion
of an object is often referred to as the object’s interface, because it acts as
the object’s interface to the rest of the program. Because other objects must
communicate with the object only through its interface, the internal portion of
the object is protected from outside tampering. And because an outside program
has no access to the internal implementation of an object, the internal
implementation can change at any time without affecting other parts of the
program.
So, you’ve learned
that encapsulation provides two primary benefits to programmers:
Implementation hiding
refers to the protection of the internal implementation of an object. An object
is composed of a public interface and a private section that can be a
combination of internal data and methods. The internal data and methods are the
sections of the object hidden. The primary benefit is that these sections can
change without affecting other parts of the program.
Modularity means that
an object can be maintained independently of other objects. Because the source
code for the internal sections of an object is maintained separately from the
interface, you are free to make modifications with confidence that your object
won’t cause problems. This makes it easier to distribute objects throughout a
system.
An object acting alone
is rarely very useful; most objects require other objects to do much of
anything. For example, the car object is pretty useless by itself with no other
interaction. Add a driver object, however, and things get more interesting!
Knowing this, it’s pretty clear that objects need some type of communication
mechanism in order to interact with each other.
Software objects
interact and communicate with each other through messages. When the driver
object wants the car object to accelerate, it sends the car object a message.
If you want to think of messages more literally, think of two people as
objects. If one person wants the other person to come closer, they send the
other person a message. More accurately, they may say to the other person “Come
here, please.” This is a message in a very literal sense. Software messages are
a little different in form, but not in theory—they tell an object what to do.
Many times the
receiving object needs—along with a message—more information so that it knows
exactly what to do. When the driver tells the car to accelerate, the car must
know by how much. This information is passed along with the message as message
parameters.
From this discussion,
you can see that messages consist of three things:
1.
The
object to receive the message (car)
2.
The
name of the action to perform (accelerate)
3.
Any
parameters the method requires (15 mph)
These three components
are sufficient information to fully describe a message for an object. Any
interaction with an object is handled by passing a message. This means that
objects anywhere in a system can communicate with other objects solely through
messages.
So you don’t get
confused, understand that “message passing” is another way of saying “method
calling.” When an object sends another object a message, it is really just
calling a method of that object. The message parameters are actually the
parameters to a method. In object-oriented programming, messages and methods
are synonymous.
Because everything
that an object can do is expressed through its methods (interface), message
passing supports all possible interactions between objects. In fact, interfaces
allow objects to send and receive messages to each other even if they reside in
different locations on a network. Objects in this scenario are referred to as
distributed objects. Java is specifically designed to support distributed
objects.
Throughout this
discussion of object-oriented programming, you’ve only dealt with the concept
of an object already existing in a system. You may be wondering how objects get
into a system in the first place. This question brings you to the most
fundamental structure in object-oriented programming: the class. A class is a
template or prototype that defines a type of object. A class is to an object
what a blueprint is to a house. Many houses may be built from a single
blueprint; the blueprint outlines the makeup of the houses. Classes work
exactly the same way, except that they outline the makeup of objects.
In the real world,
there are often many objects of the same kind. Using the house analogy, there
are many different houses around the world, but houses all share common
characteristics. In object-oriented terms, you would say that your house is a
specific instance of the class of objects known as houses. All houses have
states and behaviors in common that define them as houses. When a builder
starts building a new neighborhood of houses, he typically builds them all from
a set of blueprints. It wouldn’t be as efficient to create a new blueprint for
every single house, especially when there are so many similarities shared
between each one. The same thing goes in object-oriented software development;
why rewrite tons of code when you can reuse code that solves similar problems?
In object-oriented
programming, as in construction, it’s also common to have many objects of the
same kind that share similar characteristics. And like the blueprints for
similar houses, you can create blueprints for objects that share certain
characteristics. What it boils down to is that classes are software blueprints
for objects.
As an example, the car
class discussed earlier would contain several variables representing the state
of the car, along with implementations for the methods that enable the driver
to control the car. The state variables of the car remain hidden underneath the
interface. Each instance, or instantiated object, of the car class gets a fresh
set of state variables. This brings you to another important point: When an
instance of an object is created from a class, the variables declared by that
class are allocated in memory. The variables are then modified through the
object’s methods. Instances of the same class share method implementations but
have their own object data.
Where objects provide
the benefits of modularity and information hiding, classes provide the benefit
of reusability. Just as the builder reuses the blueprint for a house, the
software developer reuses the class for an object. Software programmers can use
a class over and over again to create many objects. Each of these objects gets
its own data but shares a single method implementation.
So, what happens if
you want an object that is very similar to one you already have, but with a few
extra characteristics? You just inherit a new class based on the class of the
similar object. Inheritance is the process of creating a new class with the
characteristics of an existing class, along with additional characteristics
unique to the new class. Inheritance provides a powerful and natural mechanism
for organizing and structuring programs.
So far, the discussion
of classes has been limited to the data and methods that make up a class. Based
on this understanding, all classes are built from scratch by defining all the
data and all the associated methods. Inheritance provides a means to create
classes based on other classes. When a class is based on another class, it
inherits all the properties of that class, including the data and methods for
the class. The class doing the inheriting is referred to as the subclass (child
class), and the class providing the information to inherit is referred to as
the superclass (parent class).
Using the car example,
child classes could be inherited from the car class for gas powered cars and
cars powered by electricity. Both new car classes share common “car”
characteristics, but they also add a few characteristics of their own. The gas
car would add, among other things, a fuel tank and a gas cap, where the
electric car might add a battery and a plug for recharging. Each subclass
inherits state information (in the form of variable declarations) from the
superclass. Figure 14.3 shows the car parent class with the gas and electric
car child classes.
Inherited car objects.
Inheriting the state
and behaviors of a superclass alone wouldn’t do all that much for a subclass.
The real power of inheritance is the ability to inherit properties and add new
ones; subclasses can add variables and methods to the ones they inherited from
the superclass. Remember, the electric car added a battery and a recharging
plug. Additionally, subclasses have the ability to override inherited methods
and provide different implementations for them. For example, the gas car would
probably be able to go much faster than the electric car. The accelerate method
for the gas car could reflect this difference.
Class inheritance is
designed to allow as much flexibility as possible. You can create inheritance
trees as deep as necessary to carry out your design. An inheritance tree, or
class hierarchy, looks much like a family tree; it shows the relationships
between classes. Unlike a family tree, the classes in an inheritance tree get
more specific as you move down the tree. The car classes in Figure 14.3 are a
good example of an inheritance tree.
By using inheritance,
you’ve learned how subclasses can allow specialized data and methods in
addition to the common ones provided by the superclass. This enables
programmers to reuse the code in the superclass many times, thus saving extra
coding effort and therefore eliminating potential bugs.
One final point to
make in regard to inheritance: It is possible and sometimes useful to create
superclasses that act purely as templates for more usable subclasses. In this
situation, the superclass serves as nothing more than an abstraction for the
common class functionality shared by the subclasses. For this reason, these
types of superclasses are referred to as abstract classes. An abstract class
cannot be instantiated, meaning that no objects can be created from an abstract
class. The reason an abstract class can’t be instantiated is that parts of it
have been specifically left unimplemented. More specifically, these parts are
made up of methods that have yet to be implemented—abstract methods.
Using the car example
once more, the accelerate method really can’t be defined until the car’s
acceleration capabilities are known. Of course, how a car accelerates is
determined by the type of engine it has. Because the engine type is unknown in
the car superclass, the accelerate method could be defined but left
unimplemented, which would make both the accelerate method and the car
superclass abstract. Then the gas and electric car child classes would
implement the accelerate method to reflect the acceleration capabilities of
their respective engines or motors.
No doubt you’re
probably about primered out by now and ready to get on with how classes work in
Java. Well, wait no longer! In Java, all classes are subclassed from a
superclass called Object. Figure 14.4 shows what the Java class hierarchy looks
like in regard to the Object superclass.
Classes derived from
the object superclass.
As you can see, all
the classes fan out from the Object base class. In Java, Object serves as the
superclass for all derived classes, including the classes that make up the Java
API.
So far, the discussion
of class declaration has been limited to creating new classes inherently
derived from Object. Deriving all your classes from Object isn’t a very good
idea, because you would have to redefine the data and methods for each class.
The way you derive classes from classes other than Object is by using the
extends keyword. The syntax for deriving a class using the extends keyword
follows:
class Identifier extends SuperClass {
ClassBody
}
Identifier refers to
the name of the newly derived class, SuperClass refers to the name of the class
you are deriving from, and ClassBody is the new class body.
Using the Alien class
as the basis for a derivation example, what if you had an Enemy class that
defined information for all enemies? You would no doubt want to derive the
Alien class from Enemy. Following is the Enemy-derived Alien class using the
extends keyword:
class Alien extends Enemy {
Color color;
int energy;
int aggression;
void morph() {
if (aggression < 10) {
// morph into a smaller size
}
else if (aggression < 20) {
// morph into a medium size
}
else {
// morph into a giant size
}
}
}
This declaration
assumes that the Enemy class declaration is readily available in the same
package as Alien. In reality, you will likely derive from classes in a lot of
different places. To derive a class from an external superclass, you must first
import the superclass using the import statement.
NOTE |
You’ll
get to packages a little later in this chapter. For now, just think of a
package as a group of related classes. |
If you had to import
the Enemy class, you would do so like this:
import Enemy;
There are times when
it is useful to override methods in derived classes. For example, if the Enemy
class had a move method, you would want the movement to vary based on the type
of enemy. Some types of enemies may fly around in specified patterns, while
other enemies may crawl in a random fashion. To allow the Alien class to
exhibit its own movement, you would override the move method with a version
specific to alien movement. The Enemy class would then look something like
this:
class Enemy {
...
void move() {
// move the enemy
}
}
Likewise, the Alien
class with the overridden move method would look something like this:
class Alien {
Color color;
int energy;
int aggression;
void move() {
// move the alien
}
void morph() {
if (aggression < 10) {
// morph into a smaller size
}
else if (aggression < 20) {
// morph into a medium size
}
else {
// morph into a giant size
}
}
}
When you create an
instance of the Alien class and call the move method, the new move method in
Alien is executed rather than the original overridden move method in Enemy.
Method overriding is a simple, yet powerful usage of object-oriented design.
Another powerful
object-oriented technique is method overloading. Method overloading enables you
to specify different types of information (parameters) to send to a method. To
overload a method, you declare another version with the same name but different
parameters.
For example, the move
method for the Alien class could have two different versions: one general
movement and one for moving to a specific location. The general version is the
one you’ve already defined, which moves the alien based on its current state.
The declaration for this version follows:
void move() {
// move the alien
}
To enable the alien to
move to a specific location, you overload the move method with a version that
takes x and y parameters, which specify the location to move. The overloaded
version of move follows:
void move(int x, int y) {
// move the alien to position x,y
}
Notice that the only
difference between the two methods is the parameter lists; the first move takes
no parameters while the second move takes two integers.
You may be wondering
how the compiler knows which method is being called in a program, when they
both have the same name. The compiler keeps up with the parameters for each
method along with the name. When a call to a method is encountered in a
program, the compiler checks the name and the parameters to determine which
overloaded method is being called. In this case, calls to the move methods are
easily distinguishable by the absence or presence of the integer parameters.
Access to variables
and methods in Java classes is accomplished through access modifiers. Access
modifiers define varying levels of access between class members and the outside
world (other objects). Access modifiers are declared immediately before the
type of a member variable or the return type of a method. There are four access
modifiers: default, public, protected, and private.
Access modifiers not
only affect the visibility of class members, but also of classes themselves.
However, class visibility is tightly linked with packages, which are covered
later in this chapter.
The default access
modifier specifies that only classes in the same package can have access to a
class’s variables and methods. So, class members with default access have a
visibility limited to other classes within the same package. There is no actual
keyword for declaring the default access modifier; it is applied by default in
the absence of an access modifier. For example, the Alien class members all had
default access, because no access modifiers were specified. Examples of a
default access member variable and method follow:
long length;
void getLength() {
return length;
}
Notice that neither
the member variable or the method supply an access modifier, so they take on
the default access modifier implicitly.
The public access
modifier specifies that class variables and methods are accessible to anyone,
both inside and outside the class. This means that public class members have
global visibility and can be accessed by any other objects. Some examples of
public member variables follow:
public int count;
public boolean isActive;
The protected access
modifier specifies that class members are accessible only to methods in that
class and subclasses of that class. This means that protected class members
have visibility limited to subclasses. Examples of a protected variable and a
protected method follow:
protected char middleInitial;
protected char getMiddleInitial() {
return middleInitial;
}
Finally, the private
access modifier, which is the most restrictive, specifies that class members
are only accessible by the class they are defined in. This means that no other
class has access to private class members, even subclasses. Some examples of
private member variables follow:
private String firstName;
private double howBigIsIt;
There are times when
you need a common variable or method for all objects of a particular class. The
static modifier specifies that a variable or method is the same for all objects
of a particular class.
Typically, new
variables are allocated for each instance of a class. When a variable is
declared as being static, it is only allocated once regardless of how many
objects are instantiated. The result is that all instantiated objects share the
same instance of the static variable. Similarly, a static method is one whose
implementation is exactly the same for all objects of a particular class. This
means that static methods only have access to static variables.
Following are some
examples of a static member variable and a static method:
static int refCount;
static int getRefCount() {
return refCount;
}
A beneficial side
effect of static members is that they can be accessed without having to create
an instance of a class. Remember the System.out.println method used in the last
chapter? Do you recall ever instantiating a System object? Of course not. out
is a static member variable of the System class, which means you can access it
without having to actually instantiate a System object.
Another useful
modifier in regard to controlling class member usage is the final modifier. The
final modifier specifies that a variable has a constant value or that a method
cannot be overridden in a subclass. To think of the final modifier literally,
it means that a class member is the final version allowed for the class.
Following are some
examples of final member variables:
final public int numDollars = 25;
final boolean amIBroke = false;
If you are coming from
the world of C++, final variables may sound kind of familiar. In fact, final
variables in Java are very similar to const variables in C++; they must always
be initialized upon declaration and their value can’t change any time
afterward.
The synchronized
modifier is used to specify that a method is thread safe. This basically means
that only one path of execution is allowed into a synchronized method at a
time. In a multithreaded environment like Java, it is possible to have many
different paths of execution running through the same code. The synchronized
modifier changes this rule by only allowing a single thread access to a method
at once, forcing the others to wait their turn. If the concept of threads and
paths of execution are totally new to you, don’t worry; they are covered in
detail in the next chapter, “Threads and Multithreading.”
The native modifier is
used to identify methods that have native implementations. The native modifier
informs the Java compiler that a method’s implementation is in an external C
file. It is for this reason that native method declarations look different from
other Java methods; they have no body. Following is an example of a native
method declaration:
native int calcTotal();
Notice that the method
declaration simply ends in a semicolon; there are no curly braces containing
Java code. This is because native methods are implemented in C code, which
resides in external C source files. To learn more about native methods, check
out Chapter 38, “Native Methods and Libraries.”
In the object-oriented
primer earlier in this chapter, you learned about abstract classes and methods.
To recap, an abstract class is a class that is partially implemented and whose
purpose is solely as a design convenience. Abstract classes are made up of one
or more abstract methods, which are methods that are declared but left bodiless
(unimplemented).
The Enemy class
discussed earlier is an ideal candidate to become an abstract class. You would
never want to actually create an enemy object because it is too general.
However, it serves a very logical purpose being a superclass for more specific
enemy classes, like the Alien class. To turn the Enemy class into an abstract
class, you use the abstract keyword, like this:
abstract class Enemy {
abstract void move();
abstract void move(int x, int y);
}
Notice the usage of
the abstract keyword before the class declaration for Enemy. This tells the
compiler that the Enemy class is abstract. Also notice that both move methods
are declared as being abstract. Because it isn’t clear how to move a generic
enemy, the move methods in Enemy have been left unimplemented (abstract).
There are a few
limitations to using abstract of which you should be aware. First, you can’t
make creation methods abstract. (You’ll learn about creation methods in the
next section covering object creation.) Second, you can’t make static methods
abstract. This stems from the fact that static methods are declared for all
classes, so there is no way to provide a derived implementation for an abstract
static method. Finally, you aren’t allowed to make private methods abstract. At
first this limitation may seem to be a little picky, but think about what it
means. When you derive a class from a superclass with abstract methods, you
must override and implement all the abstract methods or you won’t be able to
instantiate your new class, and it will remain abstract itself. Now consider
that derived classes can’t see private members of their superclass, methods
included. This results in you not being able to override and implement private
abstract methods from the superclass, which means you can’t implement
(non-abstract) classes from it. If you are limited to only deriving new
abstract classes, you won’t be able to accomplish much!
Although casting
between different data types was discussed in Chapter 12, “Java Language
Fundamentals,” the introduction of classes puts a few new twists on casting.
Casting between classes can be broken down into three different situations:
In the case of casting
from a subclass to a superclass, you can cast either implicitly or explicitly.
Implicit casting simply means you do nothing, whereas explicit casting means
you have to provide the class type in parentheses, just as with casting
fundamental data types. The cast from subclass to superclass is completely
reliable, because subclasses contain information tying them to their
superclasses. In the case of casting from a superclass to a subclass, you are
required to cast explicitly. This cast isn’t completely reliable, because the
compiler has no way of knowing if the class being cast to is a subclass of the
superclass in question. Finally, the cast from sibling to sibling isn’t allowed
in Java. If all this casting sounds a little confusing, check out the following
example:
Double d1 = new Double(5.238);
Number n = d1;
Double d2 = (Double)n;
Long l = d1; // this won’t work!
In this example, data
type wrapper objects are created and assigned to each other. If you aren’t
familiar with the data type wrapper classes, don’t worry, you’ll learn about
them in Chapter 18, “The Language Package.” For now, all you need to know is
that the Double and Long sibling classes are both derived from the Number
class. In the example, after the Double object d1 is created, it is assigned to
a Number object. This is an example of implicitly casting from a subclass to a
superclass, which is completely legal. Another Double object, d2, is then
assigned the value of the Number object. This time, an explicit cast is
required because you are casting from a superclass to a subclass, which isn’t
guaranteed to be reliable. Finally, a Long object is assigned the value of a
Double object. This is a cast between siblings and is not allowed in Java; it
will result in a compiler error.
Although most of the
design work in object-oriented programming is creating classes, you don’t
really benefit from that work until you create instances (objects) of those
classes. To use a class in a program, you must first create an instance of it.
Before getting into
the details of how to create an object, there is an important method you need
to know about: the creation method. When you create an object, you will
typically want to initialize its member variables. The creation method is a
special method you can implement in all of your classes that allows you to
initialize variables and perform any other operations when an object is created
from the class. The creation method is always given the same name as the class.
Listing 14.1 contains
the complete source code for the Alien class, which contains two creation
methods.
class Alien extends Enemy {
protected Color color;
protected int energy;
protected int aggression;
public Alien() {
color = Color.green;
energy = 100;
aggression = 15;
}
public Alien(Color c, int e, int a) {
color = c;
energy = e;
aggression = a;
}
public void move() {
// move the alien
}
public void move(int x, int y) {
// move the alien to the position x,y
}
public void morph() {
if (aggression < 10) {
// morph into a smaller size
}
else if (aggression < 20) {
// morph into a medium size
}
else {
// morph into a giant size
}
}
}
The Alien class uses
method overloading to provide two different creation methods. The first
creation method takes no parameters and initializes the member variables to
default values. The second creation method takes the color, energy, and
aggression of the alien and initializes the member variables with them. Along
with containing the new creation methods, this version of Alien uses access
modifiers to explicitly assign access levels for each member variable and
method. This is a good habit to get into.
This version of the
Alien class is located in the source file Enemy1.java on the CD-ROM, which also
includes the Enemy class. Keep in mind that these classes are just example
classes with little functionality. However, they are good examples of Java
class design and can be compiled into Java classes.
To create an instance
of a class, you declare an object variable and use the new operator. When
dealing with objects, a declaration merely states what type of object a
variable is to represent. The object isn’t actually created until the new
operator is used. Following are two examples of using the new operator to
create instances of the Alien class:
Alien anAlien = new Alien();
Alien anotherAlien;
anotherAlien = new Alien(Color.red, 56, 24);
In the first example,
the variable anAlien is declared and the object is created by using the new
operator with an assignment directly in the declaration. In the second example,
the variable anotherAlien is first declared, then the object is created and assigned
in a separate statement.
NOTE |
If
you have some C++ experience, you no doubt recognize the new operator. Even
though the new operator in Java works in a somewhat similar fashion as its
C++ counterpart, keep in mind that you must always use the new operator to
create objects in Java. This is in contrast to the C++ version of new, which
is only used when you are working with object pointers. Because Java doesn’t
support pointers, the new operator must always be used to create new objects. |
When an object falls
out of scope, it is removed from memory, or deleted. Similar to the creation
method that is called when an object is created, Java provides the ability to
define a destruction method that is called when an object is deleted. Unlike
the creation method, which takes on the name of the class, the destruction
method is called finalize. The finalize method provides a good place to perform
any type of cleanup for the object, and is defined as
void finalize() {
// cleanup
}
An example of cleanup
typically performed by Java objects is closing files. It is worth noting that
the finalize method is not guaranteed to be called by Java as soon as an object
falls out of scope. The reason for this is that Java deletes objects as part of
its system garbage collection, which occurs at inconsistent intervals. Because
an object isn’t actually deleted until Java performs a garbage collection, the
finalize method for the object isn’t called until then either.
Java provides a
powerful means of grouping related classes and interfaces together in a single
unit: packages. You’ll learn about interfaces a little later in this chapter.
Put simply, packages are groups of related classes and interfaces. Packages
provide a convenient mechanism for managing a large group of classes and
interfaces, while avoiding potential naming conflicts. The Java API itself is
implemented as a group of packages.
As an example, the
Alien and Enemy classes developed earlier would fit nicely into an Enemy
package, along with any other enemy objects. By placing classes into a package,
you also allow them to benefit from the default access modifier, which provides
classes in the same package access to each other’s class information.
The syntax for the package
statement follows:
package Identifier;
This statement must be
placed at the beginning of a compilation unit (source file), before any class
declarations. Every class located in a compilation unit with a package
statement is considered part of that package. You can still spread classes out
among separate compilation units; just be sure to include a package statement
in each.
Packages can be nested
within other packages. In this case, the Java interpreter expects the directory
structure containing the executable classes to match the package hierarchy.
When it comes time to
use classes outside of the package you are working in, you must use the import
statement. The import statement enables you to import classes from other
packages into a compilation unit. You can import individual classes or entire
packages of classes at once if you wish. The syntax for the import statement
follows:
import Identifier;
Identifier is the name
of the class or package of classes you are importing. Going back to the Alien
class, the color member variable is an instance of the Color object, which is
part of the Java AWT class library. For the compiler to understand this member
variable type, you must import the Color class. This is accomplished with
either of the following statements:
import java.awt.Color;
import java.awt.*;
The first statement
imports the specific class Color, which is located in the java.awt package. The
second statement imports all of the classes in the java.awt package. Note that
the following statement doesn’t work:
import java.*;
This statement doesn’t
work because you can’t import nested packages with the * specification. This
only works when importing all of the classes in a particular package, which is
still very useful.
There is one other way
to import objects from other packages: explicit package referencing. By
explicitly referencing the package name each time you use an object, you can
avoid using an import statement. Using this technique, the declaration of the
color member variable in Alien would like this:
java.awt.Color color;
Explicitly referencing
the package name for an external class is generally not required; it usually
only serves to clutter up the class name and can make the code harder to read.
The exception to this rule is when two packages have classes with the same
name. In this case, you are required to explicitly use the package name with
the class names.
Earlier in this
chapter you learned about access modifiers, which affect the visibility of
classes and class members. Because class member visibility is determined
relative to classes, you’re probably wondering what visibility means for a
class. Class visibility is determined relative to packages.
For example, a public
class is visible to classes in other packages. Actually, public is the only
explicit access modifier allowed for classes. Without the public access
modifier, classes default to being visible to other classes in a package but
not visible to classes outside of the package.
The last stop on this
object-oriented whirlwind tour of Java is interfaces. An interface is a
prototype for a class and is useful from a logical design perspective. This
description of an interface may sound vaguely familiar… Remember abstract
classes?
Earlier in this
chapter you learned that an abstract class is a class that has been left
partially unimplemented due to abstract methods, which are themselves
unimplemented. Interfaces are abstract classes that are left completely
unimplemented. Completely unimplemented in this case means that no methods in
the class have been implemented. Additionally, interface member data is limited
to static final variables, which means that they are constant.
The benefits of using
interfaces are much the same as the benefits of using abstract classes.
Interfaces provide a means to define the protocols for a class without worrying
with the implementation details. This seemingly simple benefit can make large
projects much easier tomanage; once interfaces have been designed, the class
development can take place without worrying about communication among classes.
Another important
usage of interfaces is the capacity for a class to implement multiple
interfaces. This is a twist on the concept of multiple inheritance, which is
supported in C++, but not in Java. Multiple inheritance enables you to derive a
class from multiple parent classes. Although powerful, multiple inheritance is
a complex and often tricky feature of C++ that the Java designers decided they
could do without. Their workaround was to allow Java classes to implement
multiple interfaces.
The major difference
between inheriting multiple interfaces and true multiple inheritance is that
the interface approach only enables you to inherit method descriptions, not
implementations. So, if a class implements multiple interfaces, that class must
provide all of the functionality for the methods defined in the interfaces.
Although this is certainly more limiting than multiple inheritance, it is still
a very useful feature. It is this feature of interfaces that separate them from
abstract classes.
The syntax for
creating interfaces follows:
interface Identifier {
InterfaceBody
}
Identifier is the name
of the interface and InterfaceBody refers to the abstract methods and static
final variables that make up the interface. Because it is assumed that all the
methods in an interface are abstract, it isn’t necessary to use the abstract
keyword.
Because an interface
is a prototype, or template, for a class, you must implement an interface to
arrive at a usable class. To implement an interface, you use the implements
keyword. The syntax for implementing a class from an interface follows:
class Identifier implements Interface {
ClassBody
}
Identifier refers to
the name of the new class, Interface is the name of the interface you are
implementing, and ClassBody is the new class body. Listing 14.2 contains the
source code for Enemy2.java, which includes an interface version of Enemy,
along with an Alien class that implements the interface.
package Enemy;
import java.awt.Color;
interface Enemy {
abstract public void move();
abstract public void move(int x, int y);
}
class Alien implements Enemy {
protected Color color;
protected int energy;
protected int aggression;
public Alien() {
color = Color.green;
energy = 100;
aggression = 15;
}
public Alien(Color c, int e, int a) {
color = c;
energy = e;
aggression = a;
}
public void move() {
// move the alien
}
public void move(int x, int y) {
// move the alien to the position x,y
}
public void morph() {
if (aggression < 10) {
// morph into a smaller size
}
else if (aggression < 20) {
// morph into a medium size
}
else {
// morph into a giant size
}
}
}