Previous Page toc Index Next Page

Chapter 14

Classes, packages, and interfaces

So far, you’ve managed to avoid the issue of object-oriented programming and how it relates to Java. This chapter aims to remedy that problem. It begins with a basic discussion of object-oriented programming in general. With this background in place, you can then move into the rest of the chapter, which covers the specific elements of the Java language that provide support for object-oriented programming—namely, classes, packages, and interfaces.

You can think of this chapter as the chapter that finishes helping you to your feet in regard to learning the Java language. Classes are the final core component of the Java language that you need to learn to be a proficient Java programmer. Once you have a solid understanding of classes and how they work in Java, you’ll be ready to write some serious Java programs. So, what are you waiting for, read on!

Object-Oriented Programming Primer

If you’ve been anywhere near the computer section of a bookstore or picked up a programming magazine in the last five years, you’ve no doubt seen the hype surrounding object-oriented programming. It’s the most popular, yet generally least understood programming technology to come about in a while, and it all revolves around the concept of an object.

You may have been wondering what the big deal is with objects and object-oriented technology. Is it something you should be concerned with, and if so, why? If you sift through the hype surrounding the whole object-oriented issue, you’ll find a very powerful technology that provides a lot of benefits to software design. The problem is that object-oriented concepts can be difficult to grasp. And you can’t embrace the benefits of object-oriented design if you don’t completely understand what they are. Because of this, a complete understanding of the theory behind object-oriented programming is usually developed over time through practice.

A lot of the confusion among developers in regard to object-oriented technology has led to confusion among computer users in general. How many products have you seen that claim they are object-oriented? Now, considering the fact that object-orientation is a software design issue, what can this statement possibly mean to a software consumer? In many ways, “object-oriented” has become to the software industry what “new and improved” is to the household cleanser industry. The truth is that the real world is already object-oriented, which is no surprise to anyone. The significance of object-oriented technology is that it enables programmers to design software in much the same way that they perceive the real world.

Now that you’ve come to terms with some of the misconceptions surrounding the object-oriented issue, try to put them aside and think of what the term object-oriented might mean to software design. This primer lays the groundwork for understanding how object-oriented design makes writing programs faster, easier, and more reliable. And it all begins with the object. Even though this chapter ultimately focuses on Java, this object-oriented primer section really applies to all object-oriented languages.

Objects

Objects are software bundles of data and the procedures that act on that data. The procedures are also known as methods. The merger of data and methods provides a means of more accurately representing real-world objects in software. Without objects, modeling a real-world problem in software requires a significant logical leap. Objects, on the other hand, enable programmers to solve real-world problems in the software domain much easier and more logically.

As evident by its name, objects are at the heart of object-oriented technology. To understand how software objects are beneficial, think about the common characteristics of all real-world objects. Lions, cars, and calculators all share two common characteristics: state and behavior. For example, the state of a lion might include color, weight, and whether the lion is tired or hungry. Lions also have certain behaviors, such as roaring, sleeping, and hunting. The state of a car includes the current speed, the type of transmission, whether it is two- or four-wheel drive, whether the lights are on, and the current gear, among other things. The behaviors for a car include turning, braking, and accelerating.

As with real-world objects, software objects also have these two common characteristics (state and behavior). To relate this back to programming terms, the state of an object is determined by its data and the behavior of an object is defined by its methods. By making this connection between real-world objects an-d software objects, you begin to see how objects help bridge the gap between the real world and the world of software inside your computer.

Because software objects are modeled after real-world objects, you can more easily represent real-world objects in object-oriented programs. You could use the lion object to represent a real lion in an interactive software zoo. Similarly, car objects would turn out very useful in a racing game. However, you don’t always have to think of software objects as modeling physical real-world objects; software objects can be just as useful for modeling abstract concepts. For example, a thread is an object used in multithreaded software systems that represents a stream of program execution. You’ll learn a lot more about threads and how they are used in Java in the next chapter, “Threads and Multithreading.”

Figure 14.1 shows a visualization of a software object, including the primary components and how they relate.

Figure FIGURE 14.1.

A software project.

The software object in Figure 14.1 clearly shows the two primary components of an object: data and methods. The figure also shows some type of communication, or access, between the data and the methods. Additionally, it shows how messages are sent through the methods, which result in responses from the object. You’ll learn more about messages and responses a little later in this chapter.

The data and methods within an object express everything that the object represents (state), along with what all it can do (behavior). A software object modeling a real-world car would have variables (data) that indicate the car’s current state: It’s traveling at 75 mph, it’s in 4th gear, and the lights are on. The software car object would also have methods that allow it to brake, accelerate, steer, change gears, and turn the lights on and off. Figure 14.2 shows what a software car object might look like.

Figure FIGURE 14.2.

A software car project.

In both Figures 14.1 and 14.2 you probably noticed the line separating the methods from the data within the object. This line is a little misleading because methods have full access tothe data within an object. The line is there to illustrate the difference between the visibility of the methods and the data to the outside. In this sense, an object’s visibility refers to what parts of the object another object has access. Because object data defaults to being invisible, or inaccessible to other objects, all interaction between objects must be handled through methods. This hiding of data within an object is called encapsulation.

Encapsulation

Encapsulation is the process of packaging an object’s data together with its methods. A powerful benefit of encapsulation is the hiding of implementation details from other objects. This means that the internal portion of an object has more limited visibility than the external portion. This results in a safeguarding of the internal portion against unwanted external access.

The external portion of an object is often referred to as the object’s interface, because it acts as the object’s interface to the rest of the program. Because other objects must communicate with the object only through its interface, the internal portion of the object is protected from outside tampering. And because an outside program has no access to the internal implementation of an object, the internal implementation can change at any time without affecting other parts of the program.

So, you’ve learned that encapsulation provides two primary benefits to programmers:

Implementation hiding refers to the protection of the internal implementation of an object. An object is composed of a public interface and a private section that can be a combination of internal data and methods. The internal data and methods are the sections of the object hidden. The primary benefit is that these sections can change without affecting other parts of the program.

Modularity means that an object can be maintained independently of other objects. Because the source code for the internal sections of an object is maintained separately from the interface, you are free to make modifications with confidence that your object won’t cause problems. This makes it easier to distribute objects throughout a system.

Messages

An object acting alone is rarely very useful; most objects require other objects to do much of anything. For example, the car object is pretty useless by itself with no other interaction. Add a driver object, however, and things get more interesting! Knowing this, it’s pretty clear that objects need some type of communication mechanism in order to interact with each other.

Software objects interact and communicate with each other through messages. When the driver object wants the car object to accelerate, it sends the car object a message. If you want to think of messages more literally, think of two people as objects. If one person wants the other person to come closer, they send the other person a message. More accurately, they may say to the other person “Come here, please.” This is a message in a very literal sense. Software messages are a little different in form, but not in theory—they tell an object what to do.

Many times the receiving object needs—along with a message—more information so that it knows exactly what to do. When the driver tells the car to accelerate, the car must know by how much. This information is passed along with the message as message parameters.

From this discussion, you can see that messages consist of three things:

  1. The object to receive the message (car)
  2. The name of the action to perform (accelerate)
  3. Any parameters the method requires (15 mph)

These three components are sufficient information to fully describe a message for an object. Any interaction with an object is handled by passing a message. This means that objects anywhere in a system can communicate with other objects solely through messages.

So you don’t get confused, understand that “message passing” is another way of saying “method calling.” When an object sends another object a message, it is really just calling a method of that object. The message parameters are actually the parameters to a method. In object-oriented programming, messages and methods are synonymous.

Because everything that an object can do is expressed through its methods (interface), message passing supports all possible interactions between objects. In fact, interfaces allow objects to send and receive messages to each other even if they reside in different locations on a network. Objects in this scenario are referred to as distributed objects. Java is specifically designed to support distributed objects.

Classes

Throughout this discussion of object-oriented programming, you’ve only dealt with the concept of an object already existing in a system. You may be wondering how objects get into a system in the first place. This question brings you to the most fundamental structure in object-oriented programming: the class. A class is a template or prototype that defines a type of object. A class is to an object what a blueprint is to a house. Many houses may be built from a single blueprint; the blueprint outlines the makeup of the houses. Classes work exactly the same way, except that they outline the makeup of objects.

In the real world, there are often many objects of the same kind. Using the house analogy, there are many different houses around the world, but houses all share common characteristics. In object-oriented terms, you would say that your house is a specific instance of the class of objects known as houses. All houses have states and behaviors in common that define them as houses. When a builder starts building a new neighborhood of houses, he typically builds them all from a set of blueprints. It wouldn’t be as efficient to create a new blueprint for every single house, especially when there are so many similarities shared between each one. The same thing goes in object-oriented software development; why rewrite tons of code when you can reuse code that solves similar problems?

In object-oriented programming, as in construction, it’s also common to have many objects of the same kind that share similar characteristics. And like the blueprints for similar houses, you can create blueprints for objects that share certain characteristics. What it boils down to is that classes are software blueprints for objects.

As an example, the car class discussed earlier would contain several variables representing the state of the car, along with implementations for the methods that enable the driver to control the car. The state variables of the car remain hidden underneath the interface. Each instance, or instantiated object, of the car class gets a fresh set of state variables. This brings you to another important point: When an instance of an object is created from a class, the variables declared by that class are allocated in memory. The variables are then modified through the object’s methods. Instances of the same class share method implementations but have their own object data.

Where objects provide the benefits of modularity and information hiding, classes provide the benefit of reusability. Just as the builder reuses the blueprint for a house, the software developer reuses the class for an object. Software programmers can use a class over and over again to create many objects. Each of these objects gets its own data but shares a single method implementation.

Inheritance

So, what happens if you want an object that is very similar to one you already have, but with a few extra characteristics? You just inherit a new class based on the class of the similar object. Inheritance is the process of creating a new class with the characteristics of an existing class, along with additional characteristics unique to the new class. Inheritance provides a powerful and natural mechanism for organizing and structuring programs.

So far, the discussion of classes has been limited to the data and methods that make up a class. Based on this understanding, all classes are built from scratch by defining all the data and all the associated methods. Inheritance provides a means to create classes based on other classes. When a class is based on another class, it inherits all the properties of that class, including the data and methods for the class. The class doing the inheriting is referred to as the subclass (child class), and the class providing the information to inherit is referred to as the superclass (parent class).

Using the car example, child classes could be inherited from the car class for gas powered cars and cars powered by electricity. Both new car classes share common “car” characteristics, but they also add a few characteristics of their own. The gas car would add, among other things, a fuel tank and a gas cap, where the electric car might add a battery and a plug for recharging. Each subclass inherits state information (in the form of variable declarations) from the superclass. Figure 14.3 shows the car parent class with the gas and electric car child classes.

Figure FIGURE 14.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 add new ones; subclasses can add variables and methods to the ones they inherited from the superclass. Remember, the electric car added a battery and a recharging plug. Additionally, subclasses have the ability to override inherited methods and provide different implementations for them. For example, the gas car would probably be able to go much faster than the electric car. The accelerate method for the gas car could reflect this difference.

Class inheritance is designed to allow as much flexibility as possible. You can create inheritance trees as deep as necessary to carry out your design. An inheritance tree, or class hierarchy, looks much like a family tree; it shows the relationships between classes. Unlike a family tree, the classes in an inheritance tree get more specific as you move down the tree. The car classes in Figure 14.3 are a good example of an inheritance tree.

By using inheritance, you’ve learned how subclasses can allow specialized data and methods in addition to the common ones provided by the superclass. This enables programmers to reuse the code in the superclass many times, thus saving extra coding effort and therefore eliminating potential bugs.

One final point to make in regard to inheritance: It is possible and sometimes useful to create superclasses that act purely as templates for more usable subclasses. In this situation, the superclass serves as nothing more than an abstraction for the common class functionality shared by the subclasses. For this reason, these types of superclasses are referred to as abstract classes. An abstract class cannot be instantiated, meaning that no objects can be created from an abstract class. The reason an abstract class can’t be instantiated is that parts of it have been specifically left unimplemented. More specifically, these parts are made up of methods that have yet to be implemented—abstract methods.

Using the car example once more, the accelerate method really can’t be defined until the car’s acceleration capabilities are known. Of course, how a car accelerates is determined by the type of engine it has. Because the engine type is unknown in the car superclass, the accelerate method could be defined but left unimplemented, which would make both the accelerate method and the car superclass abstract. Then the gas and electric car child classes would implement the accelerate method to reflect the acceleration capabilities of their respective engines or motors.

Classes

No doubt you’re probably about primered out by now and ready to get on with how classes work in Java. Well, wait no longer! In Java, all classes are subclassed from a superclass called Object. Figure 14.4 shows what the Java class hierarchy looks like in regard to the Object superclass.

Figure FIGURE 14.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.

Using the Alien class as the basis for a derivation example, what if you had an Enemy class that defined information for all enemies? You would no doubt want to derive the Alien class from Enemy. Following is the Enemy-derived Alien class using the extends keyword:

class Alien extends Enemy {

  Color color;

  int   energy;

  int   aggression;

  void morph() {

    if (aggression < 10) {

      // morph into a smaller size

    }

    else if (aggression < 20) {

      // morph into a medium size

    }

    else {

      // morph into a giant size

    }

  }

}

This declaration assumes that the Enemy class declaration is readily available in the same package as Alien. In reality, you will likely derive from classes in a lot of different places. To derive a class from an external superclass, you must first import the superclass using the import statement.

NOTE
You’ll get to packages a little later in this chapter. For now, just think of a package as a group of related classes.

If you had to import the Enemy class, you would do so like this:

import Enemy;

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 general movement and one for moving to a specific location. The general version is the one you’ve already defined, which moves the alien based on its current state. The declaration for this version follows:

void move() {

  // move the alien

}

To enable the alien to move to a specific location, you overload the move method with a version that takes x and y parameters, which specify the location to move. The overloaded version of move follows:

void move(int x, int y) {

  // move the alien to position x,y

}

Notice that the only difference between the two methods is the parameter lists; the first move takes no parameters while the second move takes two integers.

You may be wondering how the compiler knows which method is being called in a program, when they both have the same name. The compiler keeps up with the parameters for each method along with the name. When a call to a method is encountered in a program, the compiler checks the name and the parameters to determine which overloaded method is being called. In this case, calls to the move methods are easily distinguishable by the absence or presence of the integer parameters.

Access 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: default, public, protected, and private.

Access modifiers not only affect the visibility of class members, but also of classes themselves. However, class visibility is tightly linked with packages, which are covered later in this chapter.

Default

The default access modifier specifies that only classes in the same package can have access to a class’s variables and methods. So, class members with default access have a visibility limited to other classes within the same package. There is no actual keyword for declaring the default access modifier; it is applied by default in the absence of an access modifier. For example, the Alien class members all had default access, because no access modifiers were specified. Examples of a default access member variable and method follow:

long length;

void getLength() {

  return length;

}

Notice that neither the member variable or the method supply an access modifier, so they take on the default access modifier implicitly.

public

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;

protected

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;

}

private

Finally, the private access modifier, which is the most restrictive, specifies that class members are only accessible by the class they are defined in. This means that no other class has access to private class members, even subclasses. Some examples of private member variables follow:

private String firstName;

private double howBigIsIt;

The static Modifier

There are times when you need a common variable or method for all objects of a particular class. The static modifier specifies that a variable or method is the same for all objects of a particular class.

Typically, new variables are allocated for each instance of a class. When a variable is declared as being static, it is only allocated once regardless of how many objects are instantiated. The result is that all instantiated objects share the same instance of the static variable. Similarly, a static method is one whose implementation is exactly the same for all objects of a particular class. This means that static methods only have access to static variables.

Following are some examples of a static member variable and a static method:

static int refCount;

static int getRefCount() {

  return refCount;

}

A beneficial side effect of static members is that they can be accessed without having to create an instance of a class. Remember the System.out.println method used in the last chapter? Do you recall ever instantiating a System object? Of course not. out is a static member variable of the System class, which means you can access it without having to actually instantiate a System object.

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 kind of familiar. In fact, final variables in Java are very similar to const variables in C++; they must always be initialized upon declaration and their value can’t change any time afterward.

The synchronized Modifier

The synchronized modifier is used to specify that a method is thread safe. This basically means that only one path of execution is allowed into a synchronized method at a time. In a multithreaded environment like Java, it is possible to have many different paths of execution running through the same code. The synchronized modifier changes this rule by only allowing a single thread access to a method at once, forcing the others to wait their turn. If the concept of threads and paths of execution are totally new to you, don’t worry; they are covered in detail in the next chapter, “Threads and Multithreading.”

The native Modifier

The native modifier is used to identify methods that have native implementations. The native modifier informs the Java compiler that a method’s implementation is in an external C file. It is for this reason that native method declarations look different from other Java methods; they have no body. Following is an example of a native method declaration:

native int calcTotal();

Notice that the method declaration simply ends in a semicolon; there are no curly braces containing Java code. This is because native methods are implemented in C code, which resides in external C source files. To learn more about native methods, check out Chapter 38, “Native Methods and Libraries.”

Abstract Classes and Methods

In the object-oriented primer earlier in this chapter, you learned about abstract classes and methods. To recap, an abstract class is a class that is partially implemented and whose purpose is solely as a design convenience. Abstract classes are made up of one or more abstract methods, which are methods that are declared but left bodiless (unimplemented).

The Enemy class discussed earlier is an ideal candidate to become an abstract class. You would never want to actually create an enemy object because it is too general. However, it serves a very logical purpose being a superclass for more specific enemy classes, like the Alien class. To turn the Enemy class into an abstract class, you use the abstract keyword, like this:

abstract class Enemy {

  abstract void move();

  abstract void move(int x, int y);

}

Notice the usage of the abstract keyword before the class declaration for Enemy. This tells the compiler that the Enemy class is abstract. Also notice that both move methods are declared as being abstract. Because it isn’t clear how to move a generic enemy, the move methods in Enemy have been left unimplemented (abstract).

There are a few limitations to using abstract of which you should be aware. First, you can’t make creation methods abstract. (You’ll learn about creation methods in the next section covering object creation.) Second, you can’t make static methods abstract. This stems from the fact that static methods are declared for all classes, so there is no way to provide a derived implementation for an abstract static method. Finally, you aren’t allowed to make private methods abstract. At first this limitation may seem to be a little picky, but think about what it means. When you derive a class from a superclass with abstract methods, you must override and implement all the abstract methods or you won’t be able to instantiate your new class, and it will remain abstract itself. Now consider that derived classes can’t see private members of their superclass, methods included. This results in you not being able to override and implement private abstract methods from the superclass, which means you can’t implement (non-abstract) classes from it. If you are limited to only deriving new abstract classes, you won’t be able to accomplish much!

Casting

Although casting between different data types was discussed in Chapter 12, “Java Language Fundamentals,” the introduction of classes puts a few new twists on casting. Casting between classes can be broken down into three different situations:

In the case of casting from a subclass to a superclass, you can cast either implicitly or explicitly. Implicit casting simply means you do nothing, whereas explicit casting means you have to provide the class type in parentheses, just as with casting fundamental data types. The cast from subclass to superclass is completely reliable, because subclasses contain information tying them to their superclasses. In the case of casting from a superclass to a subclass, you are required to cast explicitly. This cast isn’t completely reliable, because the compiler has no way of knowing if the class being cast to is a subclass of the superclass in question. Finally, the cast from sibling to sibling isn’t allowed in Java. If all this casting sounds a little confusing, check out the following example:

Double d1 = new Double(5.238);

Number n = d1;

Double d2 = (Double)n;

Long l = d1;  // this won’t work!

In this example, data type wrapper objects are created and assigned to each other. If you aren’t familiar with the data type wrapper classes, don’t worry, you’ll learn about them in Chapter 18, “The Language Package.” For now, all you need to know is that the Double and Long sibling classes are both derived from the Number class. In the example, after the Double object d1 is created, it is assigned to a Number object. This is an example of implicitly casting from a subclass to a superclass, which is completely legal. Another Double object, d2, is then assigned the value of the Number object. This time, an explicit cast is required because you are casting from a superclass to a subclass, which isn’t guaranteed to be reliable. Finally, a Long object is assigned the value of a Double object. This is a cast between siblings and is not allowed in Java; it will result in a compiler error.

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 Creation Method

Before getting into the details of how to create an object, there is an important method you need to know about: the creation method. When you create an object, you will typically want to initialize its member variables. The creation method is a special method you can implement in all of your classes that allows you to initialize variables and perform any other operations when an object is created from the class. The creation method is always given the same name as the class.

Listing 14.1 contains the complete source code for the Alien class, which contains two creation methods.

class Alien extends Enemy {

  protected Color color;

  protected int   energy;

  protected int   aggression;

  public Alien() {

    color = Color.green;

    energy = 100;

    aggression = 15;

  }

  public Alien(Color c, int e, int a) {

    color = c;

    energy = e;

    aggression = a;

  }

  public void move() {

    // move the alien

  }

  public void move(int x, int y) {

    // move the alien to the position x,y

  }

  public void morph() {

    if (aggression < 10) {

      // morph into a smaller size

    }

    else if (aggression < 20) {

      // morph into a medium size

    }

    else {

      // morph into a giant size

    }

  }

}

The Alien class uses method overloading to provide two different creation methods. The first creation method takes no parameters and initializes the member variables to default values. The second creation method takes the color, energy, and aggression of the alien and initializes the member variables with them. Along with containing the new creation methods, this version of Alien uses access modifiers to explicitly assign access levels for each member variable and method. This is a good habit to get into.

This version of the Alien class is located in the source file Enemy1.java on the CD-ROM, which also includes the Enemy class. Keep in mind that these classes are just example classes with little functionality. However, they are good examples of Java class design and can be compiled into Java classes.

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 of using the new operator to create instances of the Alien class:

Alien anAlien = new Alien();

Alien anotherAlien;

anotherAlien = new Alien(Color.red, 56, 24);

In the first example, the variable anAlien is declared and the object is created by using the new operator with an assignment directly in the declaration. In the second example, the variable anotherAlien is first declared, then the object is created and assigned in a separate statement.

NOTE
If you have some C++ experience, you no doubt recognize the new operator. Even though the new operator in Java works in a somewhat similar fashion as its C++ counterpart, keep in mind that you must always use the new operator to create objects in Java. This is in contrast to the C++ version of new, which is only used when you are working with object pointers. Because Java doesn’t support pointers, the new operator must always be used to create new objects.

Object Destruction

When an object falls out of scope, it is removed from memory, or deleted. Similar to the creation method that is called when an object is created, Java provides the ability to define a destruction method that is called when an object is deleted. Unlike the creation method, which takes on the name of the class, the destruction method is called finalize. The finalize method provides a good place to perform any type of cleanup for the object, and is defined as

void finalize() {

  // cleanup

}

An example of cleanup typically performed by Java objects is closing files. It is worth noting that the finalize method is not guaranteed to be called by Java as soon as an object falls out of scope. The reason for this is that Java deletes objects as part of its system garbage collection, which occurs at inconsistent intervals. Because an object isn’t actually deleted until Java performs a garbage collection, the finalize method for the object isn’t called until then either.

Packages

Java provides a powerful means of grouping related classes and interfaces together in a single unit: packages. You’ll learn about interfaces a little later in this chapter. Put simply, packages are groups of related classes and interfaces. Packages provide a convenient mechanism for managing a large group of classes and interfaces, while avoiding potential naming conflicts. The Java API itself is implemented as a group of packages.

As an example, the Alien and Enemy classes developed earlier would fit nicely into an Enemy package, along with any other enemy objects. By placing classes into a package, you also allow them to benefit from the default access modifier, which provides classes in the same package access to each other’s class information.

Declaring Packages

The syntax for the package statement follows:

package Identifier;

This statement must be placed at the beginning of a compilation unit (source file), before any class declarations. Every class located in a compilation unit with a package statement is considered part of that package. You can still spread classes out among separate compilation units; just be sure to include a package statement in each.

Packages can be nested within other packages. In this case, the Java interpreter expects the directory structure containing the executable classes to match the package hierarchy.

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 once if you wish. The syntax for the import statement follows:

import Identifier;

Identifier is the name of the class or package of classes you are importing. Going back to the Alien class, the color member variable is an instance of the Color object, which is part of the Java AWT class library. For the compiler to understand this member variable type, you must import the Color class. This is accomplished with either of the following statements:

import java.awt.Color;

import java.awt.*;

The first statement imports the specific class Color, which is located in the java.awt package. The second statement imports all of the classes in the java.awt package. Note that the following statement doesn’t work:

import java.*;

This statement doesn’t work because you can’t import nested packages with the * specification. This only works when importing all of the classes in a particular package, which is still very useful.

There is one other way to import objects from other packages: explicit package referencing. By explicitly referencing the package name each time you use an object, you can avoid using an import statement. Using this technique, the declaration of the color member variable in Alien would like this:

java.awt.Color color;

Explicitly referencing the package name for an external class is generally not required; it usually only serves to clutter up the class name and can make the code harder to read. The exception to this rule is when two packages have classes with the same name. In this case, you are required to explicitly use the package name with the class names.

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.

Interfaces

The last stop on this object-oriented whirlwind tour of Java is interfaces. An interface is a prototype for a class and is useful from a logical design perspective. This description of an interface may sound vaguely familiar… Remember abstract classes?

Earlier in this chapter you learned that an abstract class is a class that has been left partially unimplemented due to abstract methods, which are themselves unimplemented. Interfaces are abstract classes that are left completely unimplemented. Completely unimplemented in this case means that no methods in the class have been implemented. Additionally, interface member data is limited to static final variables, which means that they are constant.

The benefits of using interfaces are much the same as the benefits of using abstract classes. Interfaces provide a means to define the protocols for a class without worrying with the implementation details. This seemingly simple benefit can make large projects much easier tomanage; once interfaces have been designed, the class development can take place without worrying about communication among classes.

Another important usage of interfaces is the capacity for a class to implement multiple interfaces. This is a twist on the concept of multiple inheritance, which is supported in C++, but not in Java. Multiple inheritance enables you to derive a class from multiple parent classes. Although powerful, multiple inheritance is a complex and often tricky feature of C++ that the Java designers decided they could do without. Their workaround was to allow Java classes to implement multiple interfaces.

The major difference between inheriting multiple interfaces and true multiple inheritance is that the interface approach only enables you to inherit method descriptions, not implementations. So, if a class implements multiple interfaces, that class must provide all of the functionality for the methods defined in the interfaces. Although this is certainly more limiting than multiple inheritance, it is still a very useful feature. It is this feature of interfaces that separate them from abstract classes.

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. To implement an interface, you use the implements keyword. The syntax for implementing a class from an interface follows:

class Identifier implements Interface {

  ClassBody

}

Identifier refers to the name of the new class, Interface is the name of the interface you are implementing, and ClassBody is the new class body. Listing 14.2 contains the source code for Enemy2.java, which includes an interface version of Enemy, along with an Alien class that implements the interface.

package Enemy;

import java.awt.Color;

interface Enemy {

  abstract public void move();

  abstract public void move(int x, int y);

}

class Alien implements Enemy {

  protected Color color;

  protected int   energy;

  protected int   aggression;

  public Alien() {

    color = Color.green;

    energy = 100;

    aggression = 15;

  }

  public Alien(Color c, int e, int a) {

    color = c;

    energy = e;

    aggression = a;

  }

  public void move() {

    // move the alien

  }

  public void move(int x, int y) {

    // move the alien to the position x,y

  }

  public void morph() {

    if (aggression < 10) {

      // morph into a smaller size

    }

    else if (aggression < 20) {

      // morph into a medium size

    }

    else {

      // morph into a giant size

    }

  }

}

Summary

This chapter covered the basics of object-oriented programming, along with the specific Java constructs that enable you to carry out object-oriented concepts: classes, packages, and interfaces. You learned the benefits of using classes, along with 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.


Previous Page toc Index Next Page