by Michael Morrison
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 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 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:
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:
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. |
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.
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.
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 } } }
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;
There are times when it is useful to override methods in derived classes. For example, if the Enemy class had a move() method, you would want the movement to vary based on the type of enemy. Some types of enemies may fly around in specified patterns, while other enemies may crawl in a random fashion. To allow the Alien class to exhibit its own movement, you would override the move() method with a version specific to alien movement. The Enemy class would then look something like this:
class Enemy { ... void move() { // move the enemy } }
Likewise, the Alien class with the overridden move() method would look something like this:
class Alien { Color color; int energy; int aggression; void move() { // move the alien } void morph() { if (aggression < 10) { // morph into a smaller size } else if (aggression < 20) { // morph into a medium size } else { // morph into a giant size } } }
When you create an instance of the Alien class and call the move() method, the new move() method in Alien is executed rather than the original overridden move() method in Enemy. Method overriding is a simple yet powerful usage of object-oriented design.
Another powerful object-oriented technique is method overloading. Method overloading enables you to specify different types of information (parameters) to send to a method. To overload a method, you declare another version with the same name but different parameters.
For example, the move() method for the Alien class could have two different versions: one 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 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 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 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 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 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;
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.
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 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 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."
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!
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:
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.
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.
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.
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.
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.
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.
The syntax for creating interfaces follows:
interface Identifier { InterfaceBody }
Identifier is the name of the interface and InterfaceBody refers to the abstract methods and static final variables that make up the interface. Because it is assumed that all the methods in an interface are abstract, it isn't necessary to use the abstract keyword.
Because an interface is a prototype, or template, for a class, you must implement an interface to arrive at a usable class. 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.