All Categories :
Java
Chapter 8
Classes, Packages, and Interfaces
by Michael Morrison
CONTENTS
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 hole
in your education. 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 you must learn
before becoming 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!
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?
Considering 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 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
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 is 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 includes
its 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-wheel 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; the behavior of an object is defined by its methods.
By making this connection between real-world objects and 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 can use the lion object to represent a real lion
in an interactive software zoo. Similarly, car objects would be
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 8.1 shows a visualization of a software object, including
the primary components and how they relate.
Figure 8.1: A software object.
The software object in Figure 8.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 learn
more about messages and responses later in this chapter.
The data and methods within an object express everything that
the object represents (state), along with what 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 8.2
shows what a software car object might look like.
Figure 8.2: A software car object.
In both Figures 8.1 and 8.2, notice the line separating the methods
from the data within the object. This line is a little misleading
because methods have full access to the data within an object.
The line is there to illustrate the difference between the visibility
of the methods and the data to the outside world. In this sense,
an object's visibility refers to the parts of the object
to which 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
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 arrangement results
in the 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.
Encapsulation provides two primary benefits to programmers:
- Implementation hiding. This 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 hidden sections of the object. The primary benefit is that
these sections can change without affecting other parts of the
program.
- Modularity. This 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 to other areas. This makes
it easier to distribute objects throughout a system.
Messages
An object acting alone is rarely 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 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, he or she
sends the other person a message. More accurately, he or she 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:
- The object to receive the message (car)
- The name of the action to perform (accelerate)
- 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 that 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 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.
Note |
Actually, complete support for distributed objects is a very complex issue and isn't entirely handled by the standard Java class structure. However, new extensions to Java do provide thorough support for distributed objects.
|
Classes
Throughout this discussion of object-oriented programming, you've
dealt only with the concept of an object that already exists 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 all houses 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 builders
start building a new neighborhood of houses, they typically build
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 is true 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.
Inheritance
What happens if you want an object that is very similar to one
you already have, but that has 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 (or the child class),
and the class providing the information to inherit is referred
to as the superclass (or the 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;
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 8.3 shows the car parent
class with the gas and electric car child classes.
Figure 8.3: 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 methods and add new ones;
subclasses can add variables and methods to the ones they inherited
from the superclass. Remember that 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. A group of interrelated classes is called an inheritance
tree, or class hierarchy. An inheritance tree 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. You can create inheritance
trees as deep as necessary to carry out your design, although
it is important to not go so deep that it becomes cumbersome to
see the relationship between classes. The car classes in Figure
8.3 are a good example of an inheritance tree.
By understanding the concept of inheritance, you understand how
subclasses can allow specialized data and methods in addition
to the common ones provided by the superclass. This arrangement
enables programmers to reuse the code in the superclass many times,
saving extra coding effort and 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 are 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 8.4 shows what the Java class hierarchy looks like in regard
to the Object superclass.
Figure 8.4: 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.
Declaring Classes
The syntax for declaring classes in Java follows:
class Identifier {
ClassBody
}
Identifier specifies
the name of the new class, which is by default derived from Object.
The curly braces surround the body of the class, ClassBody.
As an example, take a look at the class declaration for an Alien
class, which could be used in a space game:
class Alien {
Color color;
int energy;
int aggression;
}
The state of the Alien object
is defined by three data members, which represent the color, energy,
and aggression of the alien. It's important to notice that the
Alien class is inherently
derived from Object. So far,
the Alien class isn't all
that useful; it needs some methods. The most basic syntax for
declaring methods for a class follows:
ReturnType Identifier(Parameters) {
MethodBody
}
ReturnType specifies
the data type that the method returns, Identifier
specifies the name of the method, and Parameters
specifies the parameters to the method, if there are any. As with
class bodies, the body of a method, MethodBody,
is enclosed by curly braces. Remember that in object-oriented
design terms, a method is synonymous with a message, with the
return type being the object's response to the message. Following
is a method declaration for the morph()
method, which would be useful in the Alien
class because some aliens like to change shape:
void morph(int aggression) {
if (aggression < 10) {
// morph into a smaller size
}
else if (aggression < 20) {
// morph into a medium size
}
else {
// morph into a giant size
}
}
The morph() method is passed
an integer as the only parameter, aggression.
This value is then used to determine the size to which the alien
is morphing. As you can see, the alien morphs to smaller or larger
sizes based on its aggression.
If you make the morph() method
a member of the Alien class,
it is readily apparent that the aggression
parameter isn't necessary. This is because aggression
is already a member variable of Alien,
to which all class methods have access. The Alien
class, with the addition of the morph()
method, looks like this:
class Alien {
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
}
}
}
Deriving Classes
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.
Let's use the Alien class
introduced in the preceding section as the basis for a derivation
example. What if you had an Enemy
class that defined general information useful for all enemies?
You would no doubt want to go back and derive the Alien
class from the new Enemy
class to take advantage of the standard enemy functionality provided
by the Enemy class. 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;
Overriding Methods
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.
Overloading Methods
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 for general movement and one
for moving to a specific location. The general version is the
one you've already defined: it 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 to. 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()
method takes no parameters; the second move()
method 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 int parameters.
Access Modifiers
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: the default access modifier, public,
protected, and private.
Access modifiers affect the visibility not only 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
The default access modifier specifies that only classes in the
same package can have access to a class's variables and methods.
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 nor the method supplies
an access modifier, so each takes on the default access modifier
implicitly.
The public Access Modifier
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 object. Some examples of public
member variables follow:
public int count;
public boolean isActive;
The protected Access
Modifier
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;
}
The private Access Modifier
The private access modifier
is the most restrictive; it specifies that class members are accessible
only by the class in which they are defined. 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;
The static
Modifier
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 have access only 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 that you can access it without having to actually
instantiate a System object.
The final
Modifier
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 familiar. In fact, final
variables in Java are very similar to const
variables in C++; they must always be initialized at declaration
and their value can't change any time afterward.
The synchronized
Modifier
The synchronized modifier
is used to specify that a method is thread safe. This 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 allowing only a single thread access
to a method at once, forcing the other threads 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
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 33, "Integrating
Native Code."
Abstract Classes and Methods
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, the Enemy
class serves a very logical purpose as 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 constructors
abstract. (You'll learn about constructors in the next section,
which covers object creation.) Second, you can't make static methods
abstract. This limitation 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 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 that you can't
implement (non-abstract) classes from it. If you were limited
to deriving only new abstract classes, you couldn't accomplish
much!
Casting
Although casting between different data types was discussed in
Chapter 6, "Java Language Fundamentals,"
the introduction of classes puts a few new twists on casting.
Casting between classes can be divided into three different situations:
- Casting from a subclass to a superclass
- Casting from a superclass to a subclass
- Casting between siblings
In the case of casting from a subclass to a superclass, you can
cast either implicitly or explicitly. Implicit casting
simply means that you do nothing; explicit casting means
that you have to provide the class type in parentheses, just as
you do when casting fundamental data types. Casting from subclass
to superclass is completely reliable because subclasses contain
information tying them to their superclasses. When 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 whether 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 12,
"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 results 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.
The Constructor
Before getting into the details of how to create an object, there
is an important method you need to know about: the constructor.
When you create an object, you typically want to initialize its
member variables. The constructor is a special method you can
implement in all your classes; it allows you to initialize variables
and perform any other operation when an object is created from
the class. The constructor is always given the same name as the
class.
Listing 8.1 contains the complete source code for the Alien
class, which contains two constructors.
Listing 8.1. The Alien
class.
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 constructors. The first constructor
takes no parameters and initializes the member variables to default
values. The second constructor takes the color,
energy, and aggression
of the alien and initializes the member variables with them. As
well as containing the new constructors, this version of Alien
uses access modifiers to explicitly assign access levels to 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 that accompanies this book. The CD-ROM 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.
The new
Operator
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 that use 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 declared first; 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 used only 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 constructor that is called when an
object is created, Java provides the ability to define a destructor
that is called when an object is deleted. Unlike the constructor,
which takes on the name of the class, the destructor is called
finalize(). The finalize()
method provides a place to perform chores related to the cleanup
of an object, and is defined as follows:
void finalize() {
// cleanup
}
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. Knowing
this, it's safe to say that you shouldn't rely on the finalize()
method for anything that is time critical. In general, you will
rarely need to place code in the finalize()
method simply because the Java runtime system does a pretty good
job of cleaning up after objects on its own.
Java provides a powerful means of grouping related classes and
interfaces together in a single unit: packages. (You 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 in this chapter 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 with
access to each other's class information.
Declaring Packages
The syntax for the package
statement follows:
package Identifier;
This statement must be placed at the beginning of a compilation
unit (a single 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. When this is done,
the Java interpreter expects the directory structure containing
the executable classes to match the package hierarchy.
Importing Packages
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 the same time if you want. 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 as
an example, the color member
variable is an instance of the Color
object, which is part of the Java AWT (abstract windowing toolkit)
class library. For the compiler to understand this member variable
type, you must import the Color
class. You can do this 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 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 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 look like this:
java.awt.Color color;
Explicitly referencing the package name for an external class
is generally not required; it usually serves only 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.
Class Visibility
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 the package.
The last stop on this object-oriented whirlwind tour of Java is
a discussion of 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 because it
uses 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 about the implementation
details. This seemingly simple benefit can make large projects
much easier to manage; once interfaces have been designed, the
class development can take place without worrying about communication
among classes.
Another important use 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 enables
you to inherit only method descriptions, not implementations.
If a class implements multiple interfaces, that class must provide
all the functionality for the methods defined in the interfaces.
Although this approach is certainly more limiting than multiple
inheritance, it is still a very useful feature. It is this feature
of interfaces that separates them from abstract classes.
Declaring Interfaces
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.
Implementing Interfaces
Because an interface is a prototype, or template, for a class,
you must implement an interface to arrive at a usable class. Implementing
an interface is similar to deriving from a class, except that
you are required to implement any methods defined in the interface.
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 8.2 contains the source code for
Enemy2.java, which includes
an interface version of Enemy
along with an Alien class
that implements the interface.
Listing 8.2. The Enemy
interface and Alien
class.
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
}
}
}
This chapter covered the basics of object-oriented programming
as well as the specific Java constructs that enable you to carry
out object-oriented concepts: classes, packages, and interfaces.
You learned the benefits of using classes-and how to implement
objects from them. The communication mechanism between objects-messages
(methods)-was covered. You also learned how inheritance provides
a powerful means of reusing code and creating modular designs.
You then learned how packages enable you to logically group similar
classes together, making large sets of classes easier to manage.
Finally, you saw how interfaces provide a template for deriving
new classes in a structured manner.
You are now ready to move on to more advanced features of the
Java language, such as threads and multithreading. The next chapter
covers exactly these topics.
Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.