| Exploring Java:By Patrick Niemeyer & Joshua Peck source ref: ebookjava.html |
In this chapter, we'll get to the heart of Java and explore the object-oriented aspects of the language. Object-oriented design is the art of decomposing an application into some number of objects--self-contained application components that work together. The goal is to break the problem down into a number of smaller problems that are simpler and easier to understand. Ideally, the components can be implemented directly as objects in the Java language. And if things are truly ideal, the components correspond to well-known objects that already exist, so they don't have to be created at all.
An object design methodology is a system or a set of rules created by someone to help you identify objects in your application domain and pick the real ones from the noise. In other words, such a methodology helps you factor your application into a good set of reusable objects. The problem is that though it wants to be a science, good object-oriented design is still pretty much an art form. While you can learn from the various off-the-shelf design methodologies, none of them will help you in all situations. The truth is that experience pays.
I won't try to push you into a particular methodology here; there are shelves full of books to do that.[1] Instead, I'll provide a few hints to get you started. Here are some general design guidelines, which should be taken with a liberal amount of salt and common sense:
[1] Once you have some experience with basic object-oriented concepts, you might want to take a look at Design Patterns: Elements of Reusable Object Oriented Software by Gamma/Helm/Johnson/Vlissides (Addison-Wesley). This book catalogs useful object-oriented designs that have been refined over the years by experience. Many appear in the design of the Java APIs.
Classes are the building blocks of a Java application. A class can contain methods, variables, initialization code, and, as we'll discuss later on, even other classes. It serves as a blueprint for making class instances, which are run-time objects that implement the class structure. You declare a class with the class keyword. Methods and variables of the class appear inside the braces of the class declaration:
class Pendulum {
float mass;
float length = 1.0;
int cycles;
float position ( float time ) {
...
}
...
}
The above class, Pendulum, contains three variables: mass, length, and cycles. It also defines a method called position() that takes a float value as an argument and returns a float value. Variables and method declarations can appear in any order, but variable initializers can't use forward references to uninitialized variables.
Once we've defined the Pendulum class, we can create a Pendulum object (an instance of that class) as follows:
Pendulum p; p = new Pendulum();
Recall that our declaration of the variable p does not create a Pendulum object; it simply creates a variable that refers to an object of type Pendulum. We still have to create the object dynamically, using the new keyword. Now that we've created a Pendulum object, we can access its variables and methods, as we've already seen many times:
p.mass = 5.0; float pos = p.position( 1.0 );
Variables defined in a class are called instance variables. Every object has its own set of instance variables; the values of these variables in one object can differ from the values in another object, as shown in Figure 5.1. If you don't initialize an instance variable when you declare it, it's given a default value appropriate for its type.
In Figure 5.1, we have a hypothetical TextBook application that uses two instances of Pendulum through the reference type variables bigPendulum and smallPendulum. Each of these Pendulum objects has its own copy of mass, length, and cycles.
As with variables, methods defined in a class are instance methods. An instance method is associated with an instance of the class, but each instance doesn't really have its own copy of the method. Instead, there's just one copy of the method, but it operates on the values of the instance variables of a particular object. As you'll see later when we talk about subclassing, there's more to learn about method selection.
Inside of a class, we can access instance variables and call instance methods of the class directly by name. Here's an example that expands upon our Pendulum:
class Pendulum {
...
void resetEverything() {
cycles = 0;
mass = 1.0;
...
float startingPosition = position( 0.0 );
}
...
}
Other classes generally access members of an object through a reference, using the C-style dot notation:
class TextBook {
...
void showPendulum() {
Pendulum bob = new Pendulum();
...
int i = bob.cycles;
bob.resetEverything();
bob.mass = 1.01;
...
}
...
}
Here we have created a second class, TextBook, that uses a Pendulum object. It creates an instance in showPendulum() and then invokes methods and accesses variables of the object through the reference bob.
Several factors affect whether class members can be accessed from outside the class. You can use the visibility modifiers, public, private, and protected to restrict access; classes can also be placed into packages that affect their scope. The private modifier, for example, designates a variable or method for use only by other members inside the class itself. In the previous example, we could change the declaration of our variable cycles to private:
class Pendulum {
...
private int cycles;
...
Now we can't access cycles from TextBook:
class TextBook {
...
void showPendulum() {
...
int i = bob.cycles; // Compile time error
If we need to access cycles, we might add a getCycles() method to the Pendulum class. We'll look at access modifiers and how they affect the scope of variables and methods in detail later.
Instance variables and methods are associated with and accessed through a particular object. In contrast, members that are declared with the static modifier live in the class and are shared by all instances of the class. Variables declared with the static modifier are called static variables or class variables ; similarly, these kinds of methods are called static methods or class methods.
We can add a static variable to our Pendulum example:
class Pendulum {
...
static float gravAccel = 9.80;
...
We have declared the new float variable gravAccel as static. That means if we change its value in any instance of a Pendulum, the value changes for all Pendulum objects, as shown in Figure 5.2.
Static members can be accessed like instance members. Inside our Pendulum class, we can refer to gravAccel by name, like an instance variable:
class Pendulum {
...
float getWeight () {
return mass * gravAccel;
}
...
}
However, since static members exist in the class itself, independent of any instance, we can also access them directly through the class. We don't need a Pendulum object to set the variable gravAccel; instead we can use the class name as a reference:
Pendulum.gravAccel = 8.76;
This changes the value of gravAccel for any current or future instances. Why, you may be wondering, would we want to change the value of gravAccel? Well, perhaps we want to explore how pendulums would work on different planets. Static variables are also very useful for other kinds of data shared among classes at run-time. For instance you can create methods to register your objects so that they can communicate or you can count references to them.
We can use static variables to define constant values. In this case, we use the static modifier along with the final modifier. So, if we cared only about pendulums under the influence of the Earth's gravitational pull, we could change Pendulum as follows:
class Pendulum {
...
static final float EARTH_G = 9.80;
...
We have followed a common convention and named our constant with capital letters; C programmers should recognize the capitalization convention, which resembles C #define statements. Now the value of EARTH_G is a constant; it can be accessed by any instance of Pendulum (or anywhere, for that matter), but its value can't be changed at run-time.
It's important to use the combination of static and final only for things that are really constant. That's because, unlike other kinds of variable references, the compiler is allowed to "inline" those values within classes that reference them. This is probably OK for things like PI, which aren't likely to change for a while, but may not be ideal for other kinds of identifiers, such as we'll discuss below.
Static members are useful as flags and identifiers, which can be accessed from anywhere. These are especially useful for values needed in the construction of an instance itself. In our example, we might declare a number of static values to represent various kinds of Pendulum objects:
class Pendulum {
...
static int SIMPLE = 0, ONE_SPRING = 1, TWO_SPRING = 2;
...
We might then use these flags in a method that sets the type of a Pendulum or, more likely, in a special constructor, as we'll discuss shortly:
Pendulum pendy = new Pendulum(); pendy.setType( Pendulum.ONE_SPRING );
Remember, inside the Pendulum class, we can use static members directly by name as well:
class Pendulum {
...
void resetEverything() {
setType ( SIMPLE );
...
}
...
}
Methods appear inside class bodies. They contain local variable declarations and other Java statements that are executed by a calling thread when the method is invoked. Method declarations in Java look like ANSI C-style function declarations with two restrictions:
Here's a simple example:
class Bird {
int xPos, yPos;
double fly ( int x, int y ) {
double distance = Math.sqrt( x*x + y*y );
flap( distance );
xPos = x;
yPos = y;
return distance;
}
...
}
In this example, the class Bird defines a method, fly(), that takes as arguments two integers: x and y. It returns a double type value as a result.
The fly() method declares a local variable called distance that it uses to compute the distance flown. A local variable is temporary; it exists only within the scope of its method. Local variables are allocated and initialized when a method is invoked; they are normally destroyed when the method returns. They can't be referenced from outside the method itself. If the method is executing concurrently in different threads, each thread has its own copies of the method's local variables. A method's arguments also serve as local variables within the scope of the method.
An object created within a method and assigned to a local variable may or may not persist after the method has returned. As with all objects in Java, it depends on whether any references to the object remain. If an object is created, assigned to a local variable, and never used anywhere else, that object will no longer be referenced when the local variable is destroyed, so garbage collection will remove the object. If, however, we assign the object to an instance variable, pass it as an argument to another method, or pass it back as a return value, it may be saved by another variable holding its reference. We'll discuss object creation and garbage collection in more detail shortly.
If a local variable and an instance variable have the same name, the local variable shadows or hides the name of the instance variable within the scope of the method. In the following example, the local variables xPos and yPos hide the instance variables of the same name:
class Bird {
int xPos, yPos;
int xNest, yNest;
...
double flyToNest() {
int xPos = xNest;
int yPos = yNest:
return ( fly( xPos, yPos ) );
}
...
}
When we set the values of the local variables in flyToNest(), it has no effect on the values of the instance variables.
The special reference this refers to the current object. You can use it any time you need to refer explicitly to the current object instance. Often, you don't need to use this because the reference to the current object is implicit; this is the case with using instance variables and methods inside of a class. But we can use this to refer explicitly to instance variables in the object, even if they are shadowed.
The subsequent example shows how we can use this to allow us argument names that shadow instance variable names. This is a fairly common technique, as it saves your having to deliberately make up alternate names (as we'll try to emphasize in this book, names are important). Here's how we could implement our fly() method with shadowed variables:
class Bird {
int xPos, yPos;
double fly ( int xPos, int yPos ) {
double distance = Math.sqrt( xPos*xPos + yPos*yPos );
flap( distance );
this.xPos = xPos;
this.yPos = yPos;
return distance;
}
...
}
In this example, the expression this.xPos refers to the instance variable xPos and assigns it the value of the local variable xPos, which would otherwise hide its name. The only reason we need to use this in the above example is because we've used argument names that hide our instance variables, and we want to refer to the instance variables.
Static methods (class methods), like static variables, belong to the class and not to an individual instance of the class. What does this mean? Well, foremost, a static method lives outside of any particular class instance. It can be invoked by name, through the class name, without any objects around. Because it is not bound to a particular object instance, a static method can only directly access other static members of classes. It can't directly see any instance variables or call any instance methods, because to do so we'd have to ask: "on which instance?" Static methods can be called from instances, just like instance methods, but the important thing is that they can also be used independently.
Our fly() method uses a static method: Math.sqrt(). This method is defined by the java.lang.Math class; we'll explore this class in detail in Chapter 7, Basic Utility Classes. For now, the important thing to note is that Math is the name of a class and not an instance of a Math object (you can't even make an instance of Math). Because static methods can be invoked wherever the class name is available, class methods are closer to normal C-style functions. Static methods are particularly useful for utility methods that perform work that might be useful either independently of instances of the class or in creating instances of the class.
For example, in our Bird class we can enumerate all types of birds that can be created:
class Bird {
...
static String [] getBirdTypes( ) {
String [] types;
// Create list...
return types;
}
...
}
Here we've defined a static method getBirdTypes() that returns an array of strings containing bird names. We can use getBirdTypes() from within an instance of Bird, just like an instance method. However, we can also call it from other classes, using the Bird class name as a reference:
String [] names = Bird.getBirdTypes();
Perhaps a special version of the Bird class constructor accepts the name of a bird type. We could use this list to decide what kind of bird to create.
In the flyToNest() example, we made a point of initializing the local variables xPos and yPos. Unlike instance variables, local variables must be initialized before they can be used. It's a compile-time error to try to access a local variable without first assigning it a value:
void myMethod() {
int foo = 42;
int bar;
// bar += 1; // Compile time error, bar uninitialized
bar = 99;
bar += 1; // ok here
}
Notice that this doesn't imply local variables have to be initialized when declared, just that the first time they are referenced must be in an assignment. More subtle possibilities arise when making assignments inside of conditionals:
void myMethod {
int foo;
if ( someCondition ) {
foo = 42;
...
}
foo += 1; // Compile time error
// foo may not have been initialized
In the above example, foo is initialized only if someCondition is true. The compiler doesn't let you make this wager, so it flags the use of foo as an error. We could correct this situation in several ways. We could initialize the variable to a default value in advance or move the usage inside of the conditional. We could also make sure the path of execution doesn't reach the uninitialized variable through some other means, depending on what makes sense for our particular application. For example, we could return from the method abruptly:
int foo;
...
if ( someCondition ) {
foo = 42;
...
} else
return;
foo += 1;
In this case, there's no chance of reaching foo in an unused state and the compiler allows the use of foo after the conditional.
Why is Java so picky about local variables? One of the most common (and insidious) sources of error in C or C++ is forgetting to initialize local variables, so Java tries to help us out. If it didn't, Java would suffer the same potential irregularities as C or C++.[2]
[2] As with malloc'ed storage in C or C++, Java objects and their instance variables are allocated on a heap, which allows them default values once, when they are created. Local variables, however, are allocated on the Java virtual machine stack. As with the stack in C and C++, failing to initialize these could mean successive method calls could receive garbage values, and program execution might be inconsistent or implementation dependent.
Let's consider what happens when you pass arguments to a method. All primitive data types (e.g., int, char, float) are passed by value. Now you're probably used to the idea that reference types (i.e., any kind of object, including arrays and strings) are used through references. An important distinction (that we discussed briefly in Chapter 4) is that the references themselves (the pointers to these objects) are actually primitive types, and are passed by value too.
Consider the following piece of code:
// somewhere
int i = 0;
SomeKindOfObject obj = new SomeKindOfObject();
myMethod( i, obj );
...
void myMethod(int j, SomeKindOfObject o) {
...
}
The first chunk of code calls myMethod(), passing it two arguments. The first argument, i, is passed by value; when the method is called, the value of i is copied into the method's parameter j. If myMethod() changes the value of i, it's changing only its copy of the local variable.
In the same way, a copy of the reference to obj is placed into the reference variable o of myMethod(). Both references refer to the same object, of course, and any changes made through either reference affect the actual (single) object instance, but there are two copies of the pointer. If we change the value of, say, o.size, the change is visible through either reference. However, if myMethod() changes the reference o itself--to point to another object--it's affecting only its copy. In this sense, passing the reference is like passing a pointer in C and unlike passing by reference in C++.
What if myMethod() needs to modify the calling method's notion of the obj reference as well (i.e., make obj point to a different object)? The easy way to do that is to wrap obj inside some kind of object. A good candidate would be to wrap the object up as the lone element in an array:
SomeKindOfObject [] wrapper = { obj };
All parties could then refer to the object as wrapper[0] and would have the ability to change the reference. This is not very asthetically pleasing, but it does illustrate that what is needed is the level of indirection. Another possibility is to use this to pass a reference to the calling object.
Let's look at another piece of code that could be from an implementation of a linked list:
class Element {
public Element nextElement;
void addToList( List list ) {
list.addToList( this );
}
}
class List {
void addToList( Element element ) {
...
element.nextElement = getNextElement();
}
}
Every element in a linked list contains a pointer to the next element in the list. In this code, the Element class represents one element; it includes a method for adding itself to the list. The List class itself contains a method for adding an arbitrary Element to the list. The method addToList() calls addToList() with the argument this (which is, of course, an Element). addToList() can use the this reference to modify the Element's nextElement instance variable. The same technique can be used in conjunction with interfaces to implement callbacks for arbitrary method invocations.
Method overloading is the ability to define multiple methods with the same name in a class; when the method is invoked, the compiler picks the correct one based on the arguments passed to the method. This implies, of course, that overloaded methods must have different numbers or types of arguments. In a later section we'll look at method overriding, which occurs when we declare methods with identical signatures in different classes.
Method overloading is a powerful and useful feature. It's another form of polymorphism (ad-hoc polymorphism). The idea is to create methods that act in the same way on different types of arguments and have what appears to be a single method that operates on any of the types. The Java PrintStream's print() method is a good example of method overloading in action. As you've probably deduced by now, you can print a string representation of just about anything using the expression:
System.out.print( argument )
The variable out is a reference to an object (a PrintStream) that defines nine different versions of the print() method. They take, respectively, arguments of the following types: Object, String, char[], char, int, long, float, double, and boolean.
class PrintStream {
void print( Object arg ) { ... }
void print( String arg ) { ... }
void print( char [] arg ) { ... }
...
}
You can invoke the print() method with any of these types as an argument, and it's printed in an appropriate way. In a language without method overloading, this would require something more cumbersome, such as a separate method for printing each type of object. Then it would be your responsibility to remember what method to use for each data type.
In the above example, print() has been overloaded to support two reference types: Object and String. What if we try to call print() with some other reference type? Say, perhaps, a Date object? The answer is that since Date is a subclass of Object, the Object method is selected. When there's not an exact type match, the compiler searches for an acceptable, assignable match. Since Date, like all classes, is a subclass of Object, a Date object can be assigned to a variable of type Object. It's therefore an acceptable match, and the Object method is selected.
But what if there's more than one possible match? Say, for example, we tried to print a subclass of String called MyString. (Of course, the String class is final, so it can't be subclassed, but allow me this brief transgression for purposes of explanation.) MyString is assignable to either String or to Object. Here the compiler makes a determination as to which match is "better" and selects that method. In this case it's the String method.
The intuitive explanation is that the String class is closer to MyString in the inheritance hierarchy. It is a more specific match. A more rigorous way of specifying it would be to say that a given method is more specific than another method with respect to some arguments it wants to accept if the argument types of the first method are all assignable to the argument types of the second method. In this case, the String method is more specific to a subclass of String than the Object method because type String is assignable to type Object. The reverse is obviously not true.
If you're paying close attention, you may have noticed I said that the compiler resolves overloaded methods. Method overloading is not something that happens at run-time; this is an important distinction. It means that the selected method is chosen once, when the code is compiled. Once the overloaded method is selected, the choice is fixed until the code is recompiled, even if the class containing the called method is later revised and an even more specific overloaded method is added. This is in contrast to overridden (virtual) methods, which are located at run-time and can be found even if they didn't exist when the calling class was compiled. We'll talk about method overriding later in the chapter.
One last note about overloading. In earlier chapters, we've pointed out that Java doesn't support programmer-defined overloaded operators, and that + is the only system-defined overloaded operator. If you've been wondering what an overloaded operator is, I can finally clear up that mystery. In a language like C++, you can customize operators such as + and * to work with objects that you create. For example, you could create a class Complex that implements complex numbers, and then overload methods corresponding to + and * to add and multiply Complex objects. Some people argue that operator overloading makes for elegant and readable programs, while others say it's just "syntactic sugar" that makes for obfuscated code. The Java designers clearly espoused the later opinion when they chose not to support programmer-defined overloaded operators.
Objects in Java are allocated from a system heap space, much like malloc'ed storage in C or C++. Unlike C or C++, however, we needn't manage that memory ourselves. Java takes care of memory allocation and deallocation for you. Java explicitly allocates storage for an object when you create it with the new keyword. More importantly, objects are removed by garbage collection when they're no longer referenced.
You allocate an object by specifying the new operator with an object constructor. A constructor is a special method with the same name as its class and no return type. It's called when a new class instance is created, which gives the class an opportunity to set up the object for use. Constructors, like other methods, can accept arguments and can be overloaded (they are not, however, inherited like other methods; we'll discuss inheritance later).
class Date {
long time;
Date() {
time = currentTime();
}
Date( String date ) {
time = parseDate( date );
}
...
}
In the above example, the class Date has two constructors. The first takes no arguments; it's known as the default constructor. Default constructors play a special role in that, if we don't define any constructors for a class, an empty default constructor is supplied for us. The default constructor is what gets called whenever you create an object by calling its constructor with no arguments. Here we have implemented the default constructor so that it sets the instance variable time by calling a hypothetical method: currentTime(), which resembles the functionality of the real java.util.Date class.
The second constructor takes a String argument. Presumably, this String contains a string representation of the time that can be parsed to set the time variable.
Given the constructors above, we create a Date object in the following ways:
Date now = new Date();
Date christmas = new Date("Dec 25, 1997");
In each case, Java chooses the appropriate constructor at compile-time based on the rules for overloaded method selection.
If we later remove all references to an allocated object, it'll be garbage collected, as we'll discuss shortly:
christmas = null; // fair game for the garbage collector
Setting the above reference to null means it's no longer pointing to the "Dec 25, 1997" object. Unless that object is referenced by another variable, it's now inaccessible and can be garbage collected. Actually, setting christmas to any other value would have the same results, but using the value null is a clear way to indicate that christmas no longer has a useful value.
A few more notes about constructors. Constructors can't be abstract, synchronized, or final. Constructors can, however, be declared with the visibility modifiers public, private, or protected, to control their accessibility. We'll talk in detail about visibility modifiers later in the chapter.
A constructor can refer to another constructor in the same class or the immediate superclass using special forms of the this and super references. We'll discuss the first case here, and return to that of the superclass constructor again after we have talked more about subclassing and inheritance.
A constructor can invoke another, overloaded constructor in its class using the reference this() with appropriate arguments to select the desired constructor. If a constructor calls another constructor, it must do so as its first statement (we'll explain why in a bit):
class Car {
String model;
int doors;
Car( String m, int d ) {
model = m;
doors = d;
// other, complicated setup
...
}
Car( String m ) {
this( m, 4 );
}
...
}
In the example above, the class Car has two overloaded constructors. The first, more explicit one, accepts arguments specifying the car's model and its number of doors and uses them to set up the object. We have also provided a simpler constructor that takes just the model as an argument and, in turn, calls the first constructor with a default value of four doors. The advantage of this approach is that you can have a single constructor do all the complicated setup work; other auxiliary constructors simply feed the appropriate arguments to that constructor.
The important point is the call to this(), which must appear as the first statement our second constructor. The syntax is restricted in this way because there's a need to identify a clear chain of command in the calling of constructors. At one end of the chain, Java invokes the constructor of the superclass (if we don't do it explicitly) to ensure that inherited members are initialized properly before we proceed. There's also a point in the chain, just after the constructor of the superclass is invoked, where the initializers of the current class's instance variables are evaluated. Before that point, we can't even reference the instance variables of our class. We'll explain this situation again in complete detail after we have talked about inheritance.
For now, all you need to know is that you can invoke a second constructor only as the first statement of another constructor. In addition, you can't do anything at that point other than pass along arguments of the current constructor. For example, the following is illegal and causes a compile-time error:
Car( String m ) {
int doors = determineDoors();
this( m, doors ); // Error
} // Constructor call must be first statement
The simple model name constructor can't do any additional setup before calling the more explicit constructor. It can't even refer to an instance member for a constant value:
class Car {
...
final int default_doors = 4;
...
Car( String m ) {
this( m, default_doors ); // Error
// Referencing uninitialized variable
}
...
}
The instance variable defaultDoors above is not initialized until a later point in the chain of constructor calls, so the compiler doesn't let us access it yet. Fortunately, we can solve this particular problem by making the identifier static as well:
class Car {
...
static final int DEFAULT_DOORS = 4;
...
Car( String m ) {
this( m, DEFAULT_DOORS ); // Okay now
}
...
}
The static members of our class have been initialized for some time (since the class was first loaded), so it's safe to access them.
It's possible to declare a code block (some statements within curly braces) directly within the scope of a class. This code block doesn't belong to any method; instead, it's executed just once, at the time the object is constructed, or, in the case of a code block marked static, at the time the class is loaded.
Nonstatic code blocks can be thought of as just an extension of instance variable initialization. They are called at the time the instance variable's initializers are evaluated (after superclass construction), in the textual order in which they appear in the class source.
class MyClass {
Properties myProps = new Properties();
// set up myProps
{
myProps.put("foo", "bar);
myProps.put("boo", "gee);
}
int a = 5;
...
You can use static code blocks to initialize static class members in this way. So the static members of a class can have complex initialization just like objects:
class ColorWheel {
static Hashtable colors = new Hashtable();
// set up colors
static {
colors.put("Red", Color.red );
colors.put("Green", Color.green );
colors.put("Blue", Color.blue );
...
}
...
}
In the above example, the class ColorWheel provides a variable colors that maps the names of colors to Color objects in a Hashtable. The first time the class ColorWheel is referenced and loaded, the static components of ColorWheel are evaluated, in the order they appear in the source. In this case, the static code block simply adds elements to the colors Hashtable.
Now that we've seen how to create objects, it's time to talk about their destruction. If you're accustomed to programming in C or C++, you've probably spent time hunting down memory leaks in your code. Java takes care of object destruction for you; you don't have to worry about memory leaks, and you can concentrate on more important programming tasks.
Java uses a technique known as garbage collection to remove objects that are no longer needed. The garbage collector is Java's grim reaper. It lingers, usually in a low priority thread, stalking objects and awaiting their demise. It finds them, watches them, and periodically counts references to them to see when their time has come. When all references to an object are gone, and it's no longer accessible, the garbage-collection mechanism reclaims it and returns the space to the available pool of resources.
There are many different algorithms for garbage collection; the Java virtual machine architecture doesn't specify a particular scheme. It's worth noting, though, that current implementations of Java use a conservative mark and sweep system. Under this scheme, Java first walks through the tree of all accessible object references and marks them as alive. Then Java scans the heap looking for identifiable objects that aren't so marked. Java finds objects on the heap because they are stored in a characteristic way and have a particular signature of bits in their handles unlikely to be reproduced naturally. This kind of algorithm doesn't suffer from the problem of cyclic references, where detached objects can mutually reference each other and appear alive.
By default, the Java virtual machine is configured to run the garbage collector in a low-priority thread, so that the garbage collector runs periodically to collect stray objects. With the java interpreter that comes with the JDK, you can turn off garbage collection by using the -noasyncgc command-line option. If you do this, the garbage collector will be run only if it's requested explicitly or if the Java virtual machine runs out of memory.
A Java application can prompt the garbage collector to make a sweep explicitly by invoking the System.gc() method. An extremely time-sensitive Java application might use this to its advantage by running in an interpreter with asynchronous garbage collection deactivated and scheduling its own cleanup periods. This issue is necessarily implementation dependent, however, because on different platforms, garbage collection may be implemented in different ways. On some systems it may be continuously running in hardware.
Before a method is removed by garbage collection, its finalize() method is invoked to give it a last opportunity to clean up its act and free other kinds of resources it may be holding. While the garbage collector can reclaim memory resources, it may not take care of things like closing files and terminating network connections very gracefully or efficiently. That's what the finalize() method is for.
An object's finalize() method is guaranteed to be called once and only once before the object is garbage collected. However there's no guarantee as to if or when that will happen. Garbage collection may never run on a system that is not short of memory. It is also interesting to note that finalization and collection occur in two distinct phases of the garbage-collection process. First items are finalized, then they are collected. It is therefore possible that finalization could (intentionally or unintentionally) create a lingering reference to the object in question, postponing its garbage collection. The object could, of course, be subject to collection later, if the reference goes away, but its finalize() method would not be called again.
Lastly, unlike constructors, the finalize() methods of superclasses are not invoked automatically for you. If you need to chain together the finalization of your parent classes, you should invoke the finalize() method of your superclass, using super().finalize(). See the following sections on inheritance and overridden methods.
Classes in Java exist in a class hierarchy. A class in Java can be declared as a subclass of another class using the extends keyword. A subclass inherits variables and methods from its superclass and uses them as if they're declared within the subclass itself:
class Animal {
float weight;
...
void eat() {
...
}
...
}
class Mammal extends Animal {
int heartRate;
// inherits weight
...
void breathe() {
...
}
// inherits eat()
}
In the above example, an object of type Mammal has both the instance variable weight and the method eat(). They are inherited from Animal.
A class can extend only one other class. To use the proper terminology, Java allows single inheritance of class implementation. Later we'll talk about interfaces, which take the place of multiple inheritance as it's primarily used in C++.
A subclass can, of course, be further subclassed. Normally, subclassing specializes or refines a class by adding variables and methods:
class Cat extends Mammal {
boolean longHair;
// inherits weight and heartRate
...
void purr() {
...
}
// inherits eat() and breathe()
}
The Cat class is a type of Mammal that is ultimately a type of Animal. Cat objects inherit all the characteristics of Mammal objects and, in turn, Animal objects. Cat also provides additional behavior in the form of the purr() method and the longHair variable. We can denote the class relationship in a diagram, as shown in Figure 5.3.
A subclass inherits all members of its superclass not designated as private. As we'll discuss shortly, other levels of visibility affect what inherited members of the class can be seen from outside of the class and its subclasses, but at a minimum, a subclass always has the same set of visible members as its parent. For this reason, the type of a subclass can be considered a subtype of its parent, and instances of the subtype can be used anywhere instances of the supertype are allowed. For example:
Cat simon = new Cat(); Animal creature = simon;
The Cat simon in the above example can be assigned to the Animal type variable creature because Cat is a subtype of Animal.
In the previous section on methods, we saw that a local variable of the same name as an instance variable hides the instance variable. Similarly, an instance variable in a subclass can shadow an instance variable of the same name in its parent class, as shown in Figure 5.4.
In Figure 5.4, the variable weight is declared in three places: as a local variable in the method foodConsumption() of the class Mammal, as an instance variable of the class Mammal, and as an instance variable of the class Animal. The actual variable selected depends on the scope in which we are working.
In the above example, all variables were of the same type. About the only reason for declaring a variable with the same type in a subclass is to provide an alternate initializer. A more important use of shadowed variables involves changing their types. We could, for example, shadow an int variable with a double variable in a subclass that needs decimal values instead of integer values. We do this without changing the existing code because, as its name suggests, when we shadow variables, we don't replace them but instead mask them. Both variables still exist; methods of the superclass see the original variable, and methods of the subclass see the new version. The determination of what variables the various methods see is static and happens at compile-time.
Here's a simple example:
class IntegerCalculator {
int sum;
...
}
class DecimalCalculator extends IntegerCalculator {
double sum;
...
}
In this example, we override the instance variable sum to change its type from int to double.[3] Methods defined in the class IntegerCalculator see the integer variable sum, while methods defined in DecimalCalculator see the decimal variable sum. However, both variables actually exist for a given instance of DecimalCalculator, and they can have independent values. In fact, any methods that DecimalCalculator inherits from IntegerCalculator actually see the integer variable sum.
[3] Note that a better way to design our calculators would be to have an abstract Calculator class with two subclasses: IntegerCalculator and DecimalCalculator.
Since both variables exist in DecimalCalculator, we need to reference the variable inherited from IntegerCalculator. We do that using the super reference:
int s = super.sum
Inside of DecimalCalculator, the super keyword used in this manner refers to the sum variable defined in the superclass. I'll explain the use of super more fully in a bit.
Another important point about shadowed variables has to do with how they work when we refer to an object by way of a less derived type. For example, we can refer to a DecimalCalculator object as an IntegerCalculator. If we do so and then access the variable sum, we get the integer variable, not the decimal one:
DecimalCalculator dc = new DecimalCalculator(); IntegerCalculator ic = dc; int s = ic.sum; // Accesses IntegerCalculator sum
After this detailed explanation, you may still be wondering what shadowed variables are good for. Well, to be honest, the usefulness of shadowed variables is limited, but it's important to understand the concepts before we talk about doing the same thing with methods. We'll see a different and more dynamic type of behavior with method shadowing, or more correctly, method overriding.
In a previous section, we saw we could declare overloaded methods (i.e., methods with the same name but a different number or type of arguments) within a class. Overloaded method selection works the way I described on all methods available to a class, including inherited ones. This means that a subclass can define some overloaded methods that augment the overloaded methods provided by a superclass.
But a subclass does more than that; it can define a method that has exactly the same method signature (arguments and return type) as a method in its superclass. In that case, the method in the subclass overrides the method in the superclass and effectively replaces its implementation, as shown in Figure 5.5. Overriding methods to change the behavior of objects is another form of polymorphism (sub-type polymorphism): the one most people think of when they talk about the power of object-oriented languages.
In Figure 5.5, Mammal overrides the reproduce() method of Animal, perhaps to specialize the method for the peculiar behavior of Mammals giving live birth.[4] The Cat object's sleeping behavior is overridden to be different from that of a general Animal, perhaps to accommodate cat naps. The Cat class also adds the more unique behaviors of purring and hunting mice.
[4] We'll ignore the platypus, which is an obscure nonovoviviparous mammal.
From what you've seen so far, overridden methods probably look like they shadow methods in superclasses, just as variables do. But overridden methods are actually more powerful than that. An overridden method in Java acts like a virtual method in C++. When there are multiple implementations of a method in the inheritance hierarchy of an object, the one in the most derived class always overrides the others, even if we refer to the object by way of a less derived type. In other words, if we have a Cat instance assigned to a variable of the more general type Animal and we call its sleep() method, we get the sleep() method implemented in the Cat class, not the one in Animal:
Cat simon = new Cat(); Animal creature = simon; creature.sleep(); // Accesses Cat sleep();
In other respects, the variable creature looks like an Animal. For example, access to a shadowed variable would find the implementation in the Animal class, not the Cat class. However, because methods are virtual, the appropriate method in the Cat class can be located, even though we are dealing with an Animal object. This means we can deal with specialized objects as if they were more general types of objects and still take advantage of their specialized implementations of behavior.
Much of what you'll be doing when you're writing a Java applet or application is overriding methods defined by various classes in the Java API. For example, think back to the applets we developed in the tutorial in Chapter 2, A First Applet. Almost all of the methods we implemented for those applets were overridden methods. Recall that we created a subclass of Applet for each of the examples. Then we overrode various methods: init() set up our applet, mouseDrag() to handle mouse movement, and paint() to draw our applet.
A common programming error in Java (at least for me) is to miss and accidentally overload a method when trying to override it. Any difference in the number or type of arguments or the return type of a method produces two overloaded methods instead of a single, overridden method. Make it a habit to look twice when overriding methods.
In a previous section, I mentioned that overloaded methods are selected by the compiler at compile-time. Overridden methods, on the other hand, are selected dynamically at run-time. Even if we create an instance of a subclass our code has never seen before (perhaps a new object type loaded from the network), any overridden methods that it contains will be located and invoked at run-time to replace those that existed when we last compiled our code.
In contrast, if we load a new class that implements an additional, more specific overloaded method, our code will continue to use the implementation it discovered at compile-time. Another effect of this is that casting (i.e., explicitly telling the compiler to treat an object as one of its assignable types) affects the selection of overloaded methods, but not overridden methods.
Static methods do not belong to any object instance, they are accessed directly through a class name, so they are not dynamically selected at run-time like instance methods. That is why static methods are called "static"--they are always bound at compile time.
A static method in a superclass can be shadowed by another static method in a subclass, as long as the original method was not declared final. However, you can't override a static method with a nonstatic method. In other words, you can't change a static method into an instance method in a subclass.
When Java has to dynamically search for overridden methods in subclasses, there's a small performance penalty. In languages like C++, the default is for methods to act like shadowed variables, so you have to explicitly declare the methods you want to be virtual. Java is more dynamic, and the default is for all instance methods to be virtual. In Java you can, however, go the other direction and explicitly declare that an instance method can't be overridden, so that it will not be subject to dynamic binding and will not suffer in terms of performance. This is done with the final modifier. We have seen final used with variables to effectively make them constants. When final is applied to a method, it means that that method can't be overridden (in some sense, its implementation is constant). final can also be applied to an entire class, which means the class can't be subclassed.
When javac, the Java compiler, is run with the -O switch, it performs certain optimizations. It can inline final methods to improve performance (while slightly increasing the size of the resulting class file). private methods, which are effectively final, can also be inlined, and final classes may also benefit from more powerful optimizations.
Another kind of optimization allows you to include debugging code in your Java source without penalty. Java doesn't have a pre-processor, to explicitly control what source is included, but you can get some of the same effects by making a block of code conditional on a constant (i.e., static and final) variable. The Java compiler is smart enough to remove this code when it determines that it won't be called. For example:
static final boolean DEBUG = false;
...
static void debug (String message) {
if (DEBUG) {
System.err.println(message);
// do other stuff
...
}
}
If we compile the above code using the -O switch, the compiler will recognize that the condition on the DEBUG variable is always false, and the body of the debug() method will be optimized away. But that's not all--since debug() itself is also final it can be inlined, and an empty inlined method generates no code at all. So, when we compile with DEBUG set to false, calls to the debug() method will generate no residual code at all.
By now you should have a good, intuitive idea as to how methods are selected from the pool of potentially overloaded and overridden method names of a class. If, however, you are dying for a dry definition, I'll provide one now. If you are satisfied with your understanding, you may wish to skip this little exercise in logic.
In a previous section, I offered an inductive rule for overloaded method resolution. It said that a method is considered more specific than another if its arguments are polymorphically assignable to the arguments of the second method. We can now expand this rule to include the resolution of overridden methods by adding the following condition: to be more specific than another method, the type of the class containing the method must also be assignable to the type of the class holding the second method.
What does that mean? Well, the only classes whose types are assignable are classes in the same inheritance hierarchy. So, what we're talking about now is the set of all methods of the same name in a class or any of its parent or child classes. Since subclass types are assignable to superclass types, but not vice versa, the resolution is pushed, in the way that we expect, down the chain, towards the subclasses. This effectively adds a second dimension to the search, in which resolution is pushed down the inheritance tree towards more refined classes and, simultaneously, towards the most specific overloaded method within a given class.
When we talked about exception handling in Chapter 4, The Java Language, there's one thing I didn't mention because it wouldn't have made sense then. An important restriction on overridden methods is that they must adhere to the throws clause of the parent method's signature. If an overridden method throws an exception, the exception must be of the type specified by the parent or a subtype of that exception. Because the exception can be a subtype of the one specified by the parent, the overridden method can refine the type of exception thrown to go along with its new behavior. For example:
// A more refined exception
class MeatInedibleException extends InedibleException { ...
}
class Animal {
void eat( Food f ) throws InedibleException { ...
}
class Herbivore extends Animal {
void eat( Food f ) throws InedibleException {
if ( f instanceof Meat )
throw new MeatInedibleException();
....
}
}
In the example above, Animal specifies that it can throw an InedibleException from its eat() method. Herbivore is a subclass Animal, so it must be able to do this too. However, Herbivore's eat() method actually throws a more specific exception: MeatInedibleException. It can do this because MeatInedibleException is a subtype of InedibleException (remember that Exceptions are classes too). Our calling code's catch clause can therefore be more specific:
Animal creature = ...
try {
creature.eat( food );
} catch ( MeatInedibleException ) {
// creature can't eat this food because it's meat
} catch ( InedibleException ) {
// creature can't eat this food
}
The special references this and super allow you to refer to the members of the current object instance or those of the superclass, respectively. We have seen this used elsewhere to pass a reference to the current object and to refer to shadowed instance variables. The reference super does the same for the parents of a class. You can use it to refer to members of a superclass that have been shadowed or overridden. A common arrangement is for an overridden method in a subclass to do some preliminary work and then defer to the method of the superclass to finish the job.
class Animal {
void eat( Food f ) throws InedibleException {
// consume food
}
}
class Herbivore extends Animal {
void eat( Food f ) throws InedibleException {
// check if edible
...
super.eat( f );
}
}
In the above example, our Herbivore class overrides the Animal eat() method to first do some checking on the food object. After doing its job it simply calls the (otherwise overridden) implementation of eat() in its superclass, using super.
super prompts a search for the method or variable to begin in the scope of the immediate superclass rather than the current class. The inherited method or variable found may reside in the immediate superclass, or in a more distant one. The usage of the super reference when applied to overridden methods of a superclass is special; it tells the method resolution system to stop the dynamic method search at the superclass, instead of in the most derived class (as it otherwise does). Without super, there would be no way to access overridden methods.
As in C++, a cast explicitly tells the compiler to change the apparent type of an object reference. Unlike in C++, casts in Java are checked both at compile- and at run-time to make sure they are legal. Attempting to cast an object to an incompatible type at run-time results in a ClassCastException. Only casts between objects in the same inheritance hierarchy (and as we'll see later, to appropriate interfaces) are legal in Java and pass the scrutiny of the compiler and the run-time system.
Casts in Java affect only the treatment of references; they never change the form of the actual object. This is an important rule to keep in mind. You never change the object pointed to by a reference by casting it; you change only the compiler's (or run-time system's) notion of it.
A cast can be used to narrow the type of a reference--to make it more specific. Often, we'll do this when we have to retrieve an object from a more general type of collection or when it has been previously used as a less derived type. (The prototypical example is using an object in a Vector or Hashtable, as you'll see in Chapter 7, Basic Utility Classes.) Continuing with our Cat example:
Animal creature = ... Cat simon = ... creature = simon; // Okay // simon = creature; // Compile time error, incompatible type simon = (Cat)creature; // Okay
We can't reassign the reference in creature to the variable simon even though we know it holds an instance of a Cat (Simon). We have to perform the indicated cast. This is also called downcasting the reference.
Note that an implicit cast was performed when we went the other way to widen the reference simon to type Animal during the first assignment. In this case, an explicit cast would have been legal, but superfluous.
If casting seems complicated, here's a simple way to think about it. Basically, you can't lie about what an object is. If you have a Cat object, you can cast it to a less derived type (i.e., a type above it in the class hierarchy) such as Animal or even Object, since all Java classes are a subclass of Object. If you have an Object you know is a Cat, you can downcast the Object to be an Animal or a Cat. However, if you aren't sure if the Object is a Cat or a Dog at run-time, you should check it with instanceof before you perform the cast. If you get the cast wrong, Java throws a ClassCastException.
As I mentioned earlier, casting can affect the selection of compile-time items like variables and overloaded methods, but not the selection of overridden methods. Figure 5.6 shows the difference. As shown in the top half of the diagram, casting the reference simon to type Animal (widening it) affects the selection of the shadowed variable weight within it. However, as the lower half of the diagram indicates, the cast doesn't affect the selection of the overridden method sleep().
When we talked earlier about constructors, we discussed how the special statement this() invokes an overloaded constructor upon entry to another constructor. Similarly, the statement super() explicitly invokes the constructor of a superclass. Of course, we also talked about how Java makes a chain of constructor calls that includes the superclass's constructor, so why use super() explicitly? When Java makes an implicit call to the superclass constructor, it calls the default constructor. So, if we want to invoke a superclass constructor that takes arguments, we have to do so explicitly using super().
If we are going to call a superclass constructor with super(), it must be the first statement of our constructor, just as this() must be the first call we make in an overloaded constructor. Here's a simple example:
class Person {
Person ( String name ) {
// setup based on name
...
}
...
}
class Doctor extends Person {
Doctor ( String name, String specialty ) {
super( name );
// setup based on specialty
...
}
...
}
In this example, we use super() to take advantage of the implementation of the superclass constructor and avoid duplicating the code to set up the object based on its name. In fact, because the class Person doesn't define a default (no arguments) constructor, we have no choice but to call super() explicitly. Otherwise, the compiler would complain that it couldn't find an appropriate default constructor to call. Said another way, if you subclass a class that has only constructors that take arguments, you have to invoke one of the superclass's constructors explicitly from your subclass constructor.
Instance variables of the class are initialized upon return from the superclass constructor, whether that's due to an explicit call via super() or an implicit call to the default superclass constructor.
We can now give the full story of how constructors are chained together and when instance variable initialization occurs. The rule has three parts and is applied repeatedly for each successive constructor invoked.
As in C++, a method can be declared with the abstract modifier to indicate that it's just a prototype. An abstract method has no body; it's simply a signature definition followed by a semicolon. You can't directly use a class that contains an abstract method; you must instead create a subclass that implements the abstract method's body.
abstract vaporMethod( String name );
In Java, a class that contains one or more abstract methods must be explicitly declared as an abstract class, also using the abstract modifier :
abstract class vaporClass {
...
abstract vaporMethod( String name );
...
}
An abstract class can contain other, nonabstract methods and ordinary variable declarations; however, it can't be instantiated. To be used, it must be subclassed and its abstract methods must be overridden with methods that implement a body. Not all abstract methods have to be implemented in a single subclass, but a subclass that doesn't override all its superclass's abstract methods with actual, concrete implementations must also be declared abstract.
Abstract classes provide a framework for classes that are to be "filled in" by the implementor. The java.io.InputStream class, for example, has a single abstract method called read(). Various subclasses of InputStream implement read() in their own ways to read from their own sources. The rest of the InputStream class, however, provides extended functionality built on the simple read() method. A subclass of InputStream inherits these nonabstract methods that provide functionality based on the simple read() method that the subclass implements.
It's often desirable to specify only the prototypes for a whole set of methods and provide no implementation. In C++, this would be a purely abstract class. In Java, you should instead use an interface. An interface is like a purely abstract class; it defines a set of methods a class must implement (i.e., the behavior of a class). However, unlike in C++, a class in Java can simply say that it implements an interface and go about implementing those methods. As we'll discuss later, a class that implements an interface doesn't have to inherit from any particular part of the inheritance hierarchy or use a particular implementation.
A package is a name for a group of related classes. In Chapter 3, Tools of the Trade, we discussed how Java uses package names to locate classes during compilation and at run-time. In this sense, packages are somewhat like libraries; they organize and manage sets of classes. Packages provide more than just source code-level organization though. They also create an additional level of scope for their classes and the variables and methods within them. We'll talk about the visibility of classes in this section. In the next section, we'll discuss the effect that packages have on access to variables and methods between classes.
The source code for a Java class is called a compilation unit. A compilation unit normally contains a single class definition and is named for that class. The definition of a class named MyClass, for instance, should appear in a file named MyClass.java. For most of us, a compilation unit is just a file with a .java extension, but in an integrated development environment, it could be an arbitrary entity. For brevity here, we'll refer to a compilation unit simply as a file.
The division of classes into their own compilation units is important because, as described in Chapter 3, Tools of the Trade, the Java compiler assumes much of the responsibility of a make utility. The compiler relies on the names of source files to find and compile dependent classes. It's possible (and common) to put more than one class definition into a single file, but there are some restrictions we'll discuss shortly.
A class is declared to belong to a particular package with the package statement. The package statement must appear as the first statement in a compilation unit. There can be only one package statement, and it applies to the entire file:
package mytools.text;
class TextComponent {
...
}
In the above example, the class TextComponent is placed in the package mytools.text.
You should recall from Chapter 3, Tools of the Trade that package names are constructed in a hierarchical way, using a dot-separated naming convention. Package-name components construct a unique path for the compiler and run-time systems to locate files; however, they don't affect the contents directly in any other way. There is no such thing as a subpackage (the package name space is really flat, not hierarchical) and packages under a particular part of a package hierarchy are related only by association. For example, if we create another package called mytools.text.poetry (presumably for text classes specialized in some way to work with poetry), those classes would not be considered part of the mytools.text package and would have no special access to its members. In this sense, the package-naming convention can be misleading.
By default, a class is accessible only to other classes within its package. This means that the class TextComponent is available only to other classes in the mytools.text package. To be visible elsewhere, a class must be declared as public:
package mytools.text;
public class TextEditor {
...
}
The class TextEditor can now be referenced anywhere. There can be only a single public class defined in a compilation unit; the file must be named for that class.
By hiding unimportant or extraneous classes, a package builds a subsystem that has a well-defined interface to the rest of the world. Public classes provide a facade for the operation of the system and the details of its inner workings can remain hidden, as shown in Figure 5.7. In this sense, packages hide classes in the way classes hide private members.
Figure 5.7 shows part of the the hypothetical mytools.text package. The classes TextArea and TextEditor are declared public and can be used elsewhere in an application. The class TextComponent is part of the implementation of TextArea and is not accessible from outside of the package.
Classes within a package can refer to each other by their simple names. However, to locate a class in another package, we have to supply a qualifier. Continuing with the above example, an application refers directly to our editor class by its fully qualified name of mytools.text.TextEditor. But we'd quickly grow tired of typing such long class names, so Java gives us the import statement. One or more import statements can appear at the top of a compilation unit, beneath the package statement. The import statements list the full names of classes to be used within the file. Like a package statement, import statements apply to the entire compilation unit. Here's how you might use an import statement:
package somewhere.else;
import mytools.text.TextEditor;
class MyClass {
TextEditor editBoy;
...
}
As shown in the example above, once a class is imported, it can be referenced by its simple name throughout the code. It's also possible to import all of the classes in a package using the * notation:
import mytools.text.*;
Now we can refer to all public classes in the mytools.text package by their simple names.
Obviously, there can be a problem with importing classes that have conflicting names. If two different packages contain classes that use the same name, you just have to fall back to using fully qualified names to refer to those classes. Other than the potential for naming conflicts, there's no penalty for importing classes. Java doesn't carry extra baggage into the compiled class files. In other words, Java class files don't contain other class definitions, they only reference them.
A class that is defined in a compilation unit that doesn't specify a package falls into the large, amorphous unnamed package. Classes in this nameless package can refer to each other by their simple names. Their path at compile- and run-time is considered to be the current directory, so package-less classes are useful for experimentation, testing, and brevity in providing examples for books about Java.
One of the most important aspects of object-oriented design is data hiding, or encapsulation. By treating an object in some respects as a "black box" and ignoring the details of its implementation, we can write stronger, simpler code with components that can be easily reused.
By default, the variables and methods of a class are accessible to members of the class itself and other classes in the same package. To borrow from C++ terminology, classes in the same package are friendly. We'll call this the default level of visibility. As you'll see as we go on, the default visibility lies in the middle of the range of restrictiveness that can be specified.
The modifiers public and private, on the other hand, define the extremes. As we mentioned earlier, methods and variables declared as private are accessible only within their class. At the other end of the spectrum, members declared as public are always accessible, from any class in any package. Of course, the class that contains the methods must also be public, as we just discussed. The public members of a class should define its most general functionality--what the black box is supposed to do. Figure 5.8 illustrates the three simplest levels of visibility.
Figure 5.8 continues with the example from the previous section. Public members in TextArea are accessible from anywhere. Private members are not visible from outside the class. The default visibility allows access by other classes in the package.
The protected modifier allows special access permissions for subclasses. Contrary to how it might sound, protected is slightly less restrictive than the default level of accessibility. In addition to the default access afforded classes in the same package, protected members are visible to subclasses of the class, even if they are defined in a different package. If you are a C++ programmer and so used to more restrictive meanings for both the default and protected levels of access, this may rub you the wrong way.
Early on, the Java language allowed for certain combinations of modifiers, one of which was private protected. The meaning of private protected was to limit visibility strictly to subclasses (and remove package access). This was later deemed somewhat inconsistent and overly complex and is no longer supported.[5]
[5] The meaning of the protected modifier changed in the Beta2 release of Java, and the private protected combination appeared at the same time. They patched some potential security holes, but confused many people.
Table 5.1 summarizes the levels of visibility available in Java; it runs generally from most restrictive to least. Methods and variables are always visible within a class, so the table doesn't address those:
| Modifier | Visibility |
|---|---|
| private | None |
| none (default) | Classes in the package |
| protected | Classes in package and subclasses inside or outside the package |
| public | All classes |
There are two important (but unrelated) notes we need to add to the discussion of visibility with regards to class members in subclasses. First, when you override methods of a class in a subclass, it's not possible to reduce their visibility. While it is possible to take a private method of a class and override it to be public in a subclass, the reverse is not possible. This makes sense when you think about the fact that subtypes have to be usable as instances of their supertype (e.g., a Mammal is a type of Animal). If we could reduce the visibility of an overridden method, this would be a problem. However, we can reduce the visibility of a variable because it simply results in a shadowed variable. As with all shadowed variables, the two variables are distinct and can have separate visibilities in their different class forms.
The second point is that protected variables of a class are visible to its subclasses, but unlike C++, only in objects of the subclass's type or its subtypes. In other words, a subclass can see a protected variable from its superclass as an inherited variable, but it can't access the variable in a separate instance of the superclass itself. This can be confusing because often we forget that visibility modifiers don't resrtict between multiple instances of the same class in the same way that they do instances of different classes. Two instances of the same type of object can normally access all of each other's members, including private ones. Said another way: two instances of Cat can access all of each other's variables and methods (including private ones), but a Cat can't access a protected member in an instance of Animal, unless it can prove that the Animal is a Cat.
Interfaces are kind of like Boy Scout (or Girl Scout) merit badges. When a scout has learned to build a bird house, he can walk around wearing a little patch with a picture of one on his sleeve. This says to the world, "I know how to build a bird house." Similarly, an interface is a list of methods that define some set of behavior for an object. Any class that implements each of the methods listed in the interface can declare that it implements the interface and wear, as its merit badge, an extra type--the interface's type.
Interface types act like class types. You can declare variables to be of an interface type, you can declare arguments of methods to accept interface types, and you can even specify that the return type of a method is an interface type. In each of these cases, what is meant is that any object that implements the interface (i.e., wears the right merit badge) can fill that spot. In this sense, interfaces are orthogonal to the class hierarchy. They cut across the boundaries of what kind of object an item is and deal with it only in terms of what it can do. A class implements as many interfaces as it desires. In this way, interfaces in Java replace the need for multiple inheritance (and all of its messy side effects).
An interface looks like a purely abstract class (i.e., a class with only abstract methods). You define an interface with the interface keyword and list its methods with no bodies:
interface Driveable {
boolean startEngine();
void stopEngine();
float accelerate( float acc );
boolean turn( Direction dir );
}
The example above defines an interface called Driveable with four methods. It's acceptable, but not necessary, to declare the methods in an interface with the abstract modifier, so we haven't used it here. Interfaces define capabilities, so it's common to name interfaces after their capabilities in a passive sense. "Driveable" is a good example; "runnable" and "updateable" would be two more.
Any class that implements all the methods can then declare it implements the interface by using a special implements clause in its class definition:
class Automobile implements Driveable {
...
boolean startEngine() {
if ( notTooCold )
engineRunning = true;
...
}
void stopEngine() {
engineRunning = false;
}
float accelerate( float acc ) {
...
}
boolean turn( Direction dir ) {
...
}
...
}
The class Automobile implements the methods of the Driveable interface and declares itself Driveable using an implements clause.
As shown in Figure 5.9, another class, such as LawnMower, can also implement the Driveable interface. The figure illustrates the Driveable interface being implemented by two different classes. While it's possible that both Automobile and Lawnmower could derive from some primitive kind of vehicle, they don't have to in this scenario. This is a significant advantage of interfaces over standard multiple inheritance as implemented in C++.
After declaring the interface, we have a new type, Driveable. We can declare variables of type Driveable and assign them any instance of a Driveable object:
Automobile auto = new Automobile(); Lawnmower mower = new Lawnmower(); Driveable vehicle; vehicle = auto; vehicle.startEngine(); vehicle.stopEngine(); ... vehicle = mower; vehicle.startEngine(); vehicle.stopEngine();
Both Automobile and Lawnmower implement Driveable and can be considered of that type.
Interfaces can be used to implement callbacks in Java. A callback is a situation where you'd like to pass a reference to some behavior and have another object invoke it later. In C or C++, this is prime territory for function pointers; in Java, we'll use interfaces instead.
Consider two classes: a TickerTape class that displays data and a TextSource class that provides an information feed. We'd like our TextSource to send any new text data. We could have TextSource store a reference to a TickerTape object, but then we could never use our TextSource to send data to any other kind of object. Instead, we'd have to proliferate subclasses of TextSource that dealt with different types. A more elegant solution is to have TextSource store a reference to an interface type, TextUpdateable:
interface TextUpdateable {
receiveText( String text );
}
class TickerTape implements TextUpdateable {
TextSource source;
init() {
source = new TextSource( this );
...
}
public receiveText( String text ) {
scrollText( text ):
}
...
}
class TextSource {
TextUpdateable receiver;
TextSource( TextUpdateable r ) {
receiver = r;
}
private sendText( String s ) {
receiver.receiveText( s );
}
...
}
The only thing TextSource really cares about is finding the right method to invoke to send text. Thus, we can list that method in an interface called TextUpdateable and have our TickerTape implement the interface. A TickerTape object can then be used anywhere we need something of the type TextUpdateable. In this case, the TextSource constructor takes a TextUpdateable object and stores the reference in an instance variable of type TextUpdateable. Our TickerTape object simply passes a reference to itself as the callback for text updates, and the source can invoke its receiveText() method as necessary.
Although interfaces allow us to specify behavior without implementation, there's one exception. An interface can contain constant variable identifiers; these identifiers appear in any class that implements the interface. This functionality allows for predefined parameters that can be used with the methods:
interface Scaleable {
static final int BIG = 0, MEDIUM = 1, SMALL = 2;
void setScale( int size );
}
The Scaleable interface defines three integers: BIG, MEDIUM, and SMALL. All variables defined in interfaces are implicitly final and static; we don't have to use the modifiers here but, for clarity, we recommend you do so.
A class that implements Scaleable sees these variables:
class Box implements Scaleable {
void setScale( int size ) {
switch( size ) {
case BIG:
...
case MEDIUM:
...
case SMALL:
...
}
}
...
}
Sometimes, interfaces are created just to hold constants; anyone who implements the interfaces can see the constant names, much as if they were included by a C/C++ include file. This is a somewhat degenerate, but acceptable use of interfaces.
Sometimes completely empty interfaces will be used to serve as a marker that a class has some special property. The java.io.Serializeable interface is a good example (See Chapter 8). Classes that implement Serializable don't add any methods or variables. Their additional type simply identifies them to Java as classes that want to be able to be serialized.
Interfaces behave like classes within packages. An interface can be declared public to make it visible outside of its package. Under the default visibility, an interface is visible only inside of its package. There can be only one public interface declared in a compilation unit.
An interface can extend another interface, just as a class can extend another class. Such an interface is called a subinterface:
interface DynamicallyScaleable extends Scaleable {
void changeScale( int size );
}
The interface DynamicallyScaleable extends our previous Scaleable interface and adds an additional method. A class that implements DynamicallyScaleable must implement all methods of both interfaces.
Interfaces can't specify that they implement other interfaces, instead they are allowed to extend more than one interface. (This is multiple inheritence for interfaces). More than one superinterface can be specified with the comma operator:
interface DynamicallyScaleable extends Scaleable, SomethingElseable {
...
|
|
|