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.
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.
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.
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 (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.
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]
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 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.