Java 1.1 Unleashed
- 5 -
Classes, Packages, and Interfaces
by Michael Morrison
IN THIS CHAPTER
- Object-Oriented Programming Primer
- The Java Class Hierarchy
- Object Creation
- Object Destruction
- Packages
- Inner Classes
- Interfaces
So far, you've managed to avoid the issue of object-oriented programming and how
it relates to Java. Actually, Chapter 3, "Java Language Fundamentals,"
touched on some object-oriented programming issues, but it purposely avoided a thorough
discussion. 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!
Object-Oriented Programming Primer
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 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, thisobject-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 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 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 5.1 shows a visualization of a software object, including the primary components
and how they relate. The software object in Figure 5.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 5.2 shows what a software car object might look like.
Figure 5.1.
A software object.
Figure 5.2.
A software car object.
In both Figures 5.1 and 5.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 sections of the object hidden. 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, 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 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. You find out about these new extensions in Chapter 54, "Remote Objects
and the Java IDL System."
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 can 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 would add a battery and a plug for recharging. Each subclass inherits state information
(in the form of variable declarations) from the superclass. Figure 5.3 shows the
car parent class with the gas and electric car child classes.
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.
Figure 5.3.
Inherited car objects.
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 5.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 enables programmers to reuse the code in the superclass many times,
thus 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.
The Java Class Hierarchy
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 5.4 shows what the Java class hierarchy
looks like in regard to the Object superclass.
Figure 5.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 not only 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
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 supply an access modifier,
so they take 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 objects. 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 allocated only 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 concepts 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 32, "Integrating Native Code with the Native Method
Interface."
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 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 covering 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 (nonabstract) 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 3, "Java
Language Fundamentals," the introduction of classes puts a few new twists on
casting. Casting between classes can be broken 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 Chap- ter 10, "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.
Object Creation
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 operations when an object is created from the class. The constructor is
always given the same name as the class.
Listing 5.1 contains the complete source code for the Alien class, which
contains two constructors.
Listing 5.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 Enemy1.java source code file
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 to 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.
Object Destruction
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 the 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.
Packages
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 wildcard works only 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 of the package.
Inner Classes
Most Java classes are defined at the package level, meaning that each class is
a member of a particular package. If you don't explicitly specify a package association
for a class, the default package is assumed. Classes defined at the package level
are known as top-level classes. Before Java 1.1, top-level classes were the only
types of classes supported. However, Java 1.1 has ushered in a more open-minded approach
to class definition. Java 1.1 supports inner classes, which are classes that can
be defined in any scope. This means that a class can be defined as a member of another
class, within a block of statements, or anonymously within an expression.
Although they may seem to be a minor enhancement to the Java language, inner classes
actually represent a significant modification. When you consider that inner classes
are the only modification to the Java language itself in Java 1.1, you'll start to
get the picture. The rest of the enhancements introduced with Java 1.1 came in the
form of new APIs. Why bother changing the language itself for something as seemingly
abstract as inner classes? The answer to this question is not exactly simple. Rather
than get into a discussion beyond the scope of this chapter, let me sum up the need
for inner classes by saying that the new Java 1.1 AWT event model specifically needed
a mechanism like the one provided by inner classes to function properly.
Rules governing the scope of an inner class closely match those governing variables.
An inner class's name is not visible outside its scope, except in a fully qualified
name (which helps in structuring classes within a package). The code for an inner
class can use simple names from enclosing scopes--including class and member variables
of enclosing classes--as well as local variables of enclosing blocks. In addition,
you can define a top-level class as a static member of another top-level class. Unlike
an inner class, a top-level class cannot directly use the instance variables of any
other class. The ability to nest classes in this way allows any top-level class to
provide a package-style organization for a logically related group of secondary top-level
classes.
Following is a simple example of an inner class:
public class Outer {
int x, y;
public int calcArea() {
return x * y;
}
class Inner {
int z;
public int calcVolume() {
return calcArea() * z;
}
}
}
In this example, an inner class named Inner is declared within a class
called Outer. As you can see, the inner class declaration looks just like
a normal (outer) class declaration. Admittedly, this example isn't too useful, but
it nevertheless gives you an idea of how inner classes are structured.
NOTE: The support for inner classes in
Java 1.1 was provided entirely by the Java compiler and did not require any changes
to the Java virtual machine (VM). This is a major part of the reason why the Java
architects were willing to modify the Java language to support inner classes because
they knew it wouldn't impact the VM.
Interfaces
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 5.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 5.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
}
}
}
Summary
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),
were 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.
|