home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Exploring Java

Previous Chapter 5
Objects in Java
Next
 

5.2 Methods

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:

  • A method in Java always specifies a return type (there's no default). The returned value can be a primitive numeric type, a reference type, or the type void, which indicates no returned value.

  • A method always has a fixed number of arguments. The combination of method overloading and true arrays removes most of the need for a variable number of arguments. These techniques are type-safe and easier to use than C's variable argument list mechanism.

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.

Local Variables

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.

Shadowing

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.

this

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

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.

Local Variable Initialization

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.

Argument Passing and References

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

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.


Previous Home Next
Classes Book Index Object Creation

Java in a Nutshell Java Language Reference Java AWT Java Fundamental Classes Exploring Java