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 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 lessens 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, which 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.

The “this” reference

You can use the special reference this any time you need to refer explicitly to the current object. At times 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 a class. But we can use this to refer explicitly to instance variables in the object, even if they are shadowed. The following example shows how we can use this to allow argument names that shadow instance variable names. This is a fairly common technique, as it saves your having to make up alternative names. 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 previous 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 directly access only other static members of the class. 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( ), which is defined by the java.lang.Math class; we’ll explore this class in detail in Chapter 9. For now, the important thing to note is that Math is the name of a class and not an instance of a Math object. (It so happens that you can’t even make an instance of the Math class.) 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 is useful either independently of instances or in creating instances. 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( ), which 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.

Initializing Local Variables

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;  Would cause 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 be initialized
}

In this 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 uninitialized state, so 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++.[18]

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 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:

...  
    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, so any changes made through either reference affect the actual (single) object instance. If we change the value of, say, o.size, the change is visible either as o.size (inside myMethod( )) or as obj.size (in the calling method). However, if myMethod( ) changes the reference o itself—to point to another object—it’s affecting only its copy. It doesn’t affect the variable obj, which still refers to the original object. 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. One example would be to wrap the object up as the lone element in an array:

SomeKindOfObject [] wrapper = new SomeKindOfObject [] { obj };

All parties could then refer to the object as wrapper[0] and would have the ability to change the reference. This is not aesthetically 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. In that case, the calling object serves as the wrapper for the reference. Let’s look at a 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 that overloaded methods must have different numbers or types of arguments. (In Chapter 6, we’ll look at method overriding, which occurs when we declare methods with identical signatures in different classes.)

Method overloading (also called ad-hoc polymorphism) is a powerful and useful feature. The idea is to create methods that act in the same way on different types of arguments. This creates the illusion that a single method can operate on any of the types. The print( ) method in the standard PrintStream class 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 this 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. The versions take 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 uniquely named 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 previous 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? 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.

What if there’s more than one possible match? Say, for example, we tried to print a subclass of String called MyString. (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 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 not true.

If you’re paying close attention, you may have noticed we said that the compiler resolves overloaded methods. Method overloading is not something that happens at runtime; 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 runtime 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 latter opinion when they chose not to support programmer-defined overloaded operators.



[18] 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.

Get Learning Java now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.