Section 5.2. Methods


5.2. Methods

Methods appear inside class bodies. They contain local variable declarations and other Java statements that are executed when the method is invoked. Methods may return a value to the caller. They always specify a return type, which can be a primitive type, a reference type, or the type void, which indicates no returned value. Methods may take arguments, which are values supplied by the caller of the method.

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, using the return keyword.

Prior to Java 5.0, the number and type of method arguments was fixed. Now Java has variable-length argument lists for methods, which allow a method to specify that it can take any number of arguments and sort them itself at runtime. We provide more details later in this chapter.

5.2.1. 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 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 version 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 is no longer referenced when the local variable disappears from scope, so garbage collection removes 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.

5.2.2. 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.

5.2.2.1 The "this" reference

You can use the special reference this any time you need to refer explicitly to the current object or a member of the current object. Often you don't need to use this, because the reference to the current object is implicit; such is the case when using unambiguously named instance variables inside a class. But we can use this to refer explicitly to instance variables in our 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 because it saves 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;  // instance var = local vra             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. You can also use the this reference any time you want to pass a reference to "the current" enclosing object to some other method; we'll show examples of that later.

5.2.3. Static Methods

Static methods (class methods), like static variables, belong to the class and not to individual instances 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 (static variables and other static methods) 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 11. 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 C-style functions. Static methods are particularly useful for utility methods that perform work that is useful either independently of instances or in working on instances. For example, in our Bird class, we could enumerate all of the available types of birds that can be created:

     class Bird {         ...         static String [] getBirdTypes( ) { ... }     }

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:

     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.

Static methods also play an important role in various design patterns, where you limit the use of the new operator for a class to one methoda static method called a factory method. We'll talk more about object construction later, but suffice it to say that it's common to see usage like this:

     Bird bird = Bird.createBird( "pigeon" );

5.2.4. 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;  // compile-time error, bar uninitialized         bar = 99;         bar += 1;  // would be 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 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 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 simply make sure that we assign foo a value in both the if and else branch. Or 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 errors 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++.[*]

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

5.2.5. Argument Passing and References

In the beginning of Chapter 4 we described the distinction between primitive types, which are passed by value (by copying), and objects, which are passed by reference. Now that we've got a better handle on methods in Java, let's walk through an example:

     void myMethod( int j, SomeKindOfObject o ) {         ...     }     // use the method     int i = 0;     SomeKindOfObject obj = new SomeKindOfObject( );     myMethod( i, obj );

The 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 (a local variable to it) named j. If myMethod( ) changes the value of j, 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 itselfto point to another objectit's affecting only its local variable reference. It doesn't affect the caller's 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. For example, we could 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.

5.2.6. Wrappers for Primitive Types

As we described in Chapter 4, there is a schism in the Java world between class types (i.e., objects) and primitive types (i.e., numbers, characters, and Boolean values). Java accepts this tradeoff simply for efficiency reasons. When you're crunching numbers, you want your computations to be lightweight; having to use objects for primitive types complicates performance optimizations. For the times you want to treat values as objects, Java supplies a standard wrapper class for each of the primitive types, as shown in Table 5-1.

Table 5-1. Primitive type wrappers

Primitive

Wrapper

Void

java.lang.Void

Boolean

java.lang.Boolean

Char

java.lang.Character

Byte

java.lang.Byte

Short

java.lang.Short

Int

java.lang.Integer

Long

java.lang.Long

Float

java.lang.Float

Double

java.lang.Double


An instance of a wrapper class encapsulates a single value of its corresponding type. It's an immutable object that serves as a container to hold the value and let us retrieve it later. You can construct a wrapper object from a primitive value or from a String representation of the value. The following statements are equivalent:

     Float pi = new Float( 3.14 );     Float pi = new Float( "3.14" );

The wrapper constructors throw a NumberFormatException when there is an error in parsing a string.

Each of the numeric type wrappers implements the java.lang.Number interface, which provides "value" methods access to its value in all the primitive forms. You can retrieve scalar values with the methods doubleValue( ), floatValue( ), longValue( ), intValue( ), shortValue( ), and byteValue( ):

     Double size = new Double ( 32.76 );     double d = size.doubleValue( );     // 32.76     float f = size.floatValue( );       // 32.76     long l = size.longValue( );         // 32     int i = size.intValue( );           // 32

This code is equivalent to casting the primitive double value to the various types.

The most common need for a wrapper is when you want to pass a primitive value to a method that requires an object. For example, in Chapter 11, we'll look at the Java Collections API, a sophisticated set of classes for dealing with object groups, such as lists, sets, and maps. All the Collections APIs work on object types, so primitives must be wrapped when stored in them. We'll see in the next section that Java 5.0 makes this wrapping process automatic. For now, let's do it ourselves. As we'll see, a List is an extensible collection of Objects. We can use wrappers to hold numbers in a List (along with other objects):

     // Simple Java code     List myNumbers = new ArrayList( );     Integer thirtyThree = new Integer( 33 );     myNumbers.add( thirtyThree );

Here we have created an Integer wrapper object so that we can insert the number into the List, using add( ). Later, when we are extracting elements from the List, we can recover the int value as follows:

     // Simple Java code     Integer theNumber = (Integer)myNumbers.get(0);     int n = theNumber.intValue( );           // 33

In Java 5.0, the code is more concise and safer. The usage of the wrapper class is mostly hidden from us by the compiler, but it is still being used internally:

     // Java code using autoboxing and generics     List<Integer> myNumbers = new ArrayList<Integer>( );     myNumbers.add( 33 );     int n = myNumbers.get( 0 );

This example will make more sense as you read on.

5.2.7. Autoboxing and Unboxing of Primitives

In Java 5.0, the compiler automatically wraps primitives in their wrapper types and unwraps them where appropriate. This process is called autoboxing and unboxing the primitive. It happens when primitives are used as arguments and return values in methods and on simple assignment to variables. For example:

     // Simple assignments     Integer integer = 5;     int i = new Integer(5);     // Method arguments and return types     Double multiply( Double a, Double b ) {         return a.doubleValue( ) * b.doubleValue( );     }     double d = multiply( 5.0, 5.0 );

In the first case, Java simply wrapped the value 5 into an Integer for us. In the second case, it unwrapped our (unnecessary) Integer object to its primitive value. Next, we have a method that multiplies two Double wrapper objects and returns the result as a Double wrapper. This example actually has three cases of boxing and one case of unboxing. First, the two double primitive values are boxed to Double types in order to call the method. Next, the return statement of the method is actually being called on a primitive double value, which the compiler turns into a Double before it leaves the method. Finally, the compiler unboxes the return value on assignment to the primitive double variable d. This example is not a compelling case, but it serves to exercise all of the features for us.

5.2.7.1 Performance implications of boxing

Gauging performance is tricky. For the vast majority of applications, the time it takes to perform tasks like creating a small object or calling a method is miniscule compared to other factors, such as I/O, user interaction, or the actual logic of the application. As a general rule, it's not wise to worry too much about these detailed performance issues until the application is mature (no premature optimization). However, we can anticipate that in performance-critical areas allowing Java to box and unbox primitives will not be as fast as using primitives directly. One aspect of this to consider is how many new objects are being created and reclaimed by the garbage collector. While in general Java may be forced to create a new object for each boxed primitive, there are optimizations for a small range of values. Java guarantees that the Boolean values true and false, "small" valued numeric types ranging from 0 to 127 for bytes and chars, and from -128 to 127 for shorts and integers are interned. Saying that they are interned means that instead of creating a new object each time, Java reuses the same object on subsequent boxings. This is safe because primitive wrappers are immutable and cannot be changed.

     Integer i = 4;     Integer j = 4;     System.out.println( i == j ); // True only for small values.

The effect of this, as shown in this code snippet, is that for small identical values the boxed primitives are actually the same object. Java also attempts to intern string values in Java classes. We'll talk about that in Chapter 10.

5.2.8. Variable-Length Argument Lists

As we mentioned earlier, Java 5.0 introduced variable-length argument lists or "varargs" for methods. The most common example usage of varargs is for the new printf( ) style printing method, which allows any number of tags to be embedded in a string and takes an argument for each tag, to be printed. For example:

     System.out.printf("My name is %s and my age is %s\n", "Bob", 21 );     System.out.printf("Get the %s out of %s before I %s\n", item, place, action );

Varargs allow the printf( ) method to accept any number of items to print (from zero to dozens, as awkward as that would be).

A method accepting a variable argument list is equivalent to a method accepting an array of some type of object. The difference is that the compiler makes the method call accept individual, comma-separated values, and then packs them into the array for us. The syntax for declaring the varargs method uses ellipses (...) where the square brackets of an array might go. For example:

     void printObjects( Object ... list ) {         // list is an Object []         for( Object o : list )             System.out.println( o );     }

Inside the printObjects( ) method, the variable list is actually an Object [] type. We could find out how many arguments were passed to us by asking the array for its length in the usual way:

     System.out.println( "Number of arguments:" + list.length );

If the caller passed no arguments, the array will be empty.

In the case of our printObjects( ) method, we could pass a mix of primitive values as well as object types because the compiler would automatically box the primitives to their wrapper types for us before placing them into the Object [].

The variable argument list does not have to be of type Object. It can be of any type, including primitive types. For example:

     printInts( int ... list ) {         // list is an int []     }     // usage     printInts( 1, 2, 3, 4 );     printStrings( String ... list ) {         // list is a String []     }     // usage     printStrings( "foo", "bar", "gee" );

The printInts( ) method receives an int [] array of primitive int values. The printStrings( ) method receives a String [] as its argument. The actual arguments must all be assignable (possibly after numeric promotion or boxing) to the type of the variable argument list. In other words, the printInts( ) method can only be called with numbers assignable to int, and the printStrings( ) method can only be called with Strings.

Varargs methods may also have any number of fixed arguments before the varargs declaration. This is how the printf( ) method guarantees that its first argument is the format string:

     void printf( String format, Object ... args ) { ... }

Of course, a method can have only one varargs declaration and it must come last in the method signature.

5.2.9. 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 many types of arguments. 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, "overloaded" 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 requires something more cumbersome, such as a uniquely named method for printing each type of object. In that case, it's 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, 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? For example, we try to print a subclass of String called MyString. (The String class is final, so it can't really be subclassed, but let's use our imaginations.) 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 slightly 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 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.



    Learning Java
    Learning Java
    ISBN: 0596008732
    EAN: 2147483647
    Year: 2005
    Pages: 262

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net