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.
Our method has a fixed number of arguments (two); however, methods can have variable-length argument lists, which allow the 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.
Our 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 (the block) 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; the only difference
is that they are initialized by being passed in from the caller 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 of an object, 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 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.
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, syntactically 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 method—a
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"
);
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 out. If it didn’t, Java would suffer the same potential irregularities as C or C++.[13]
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 both 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 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.
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.
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 makes this wrapping process automatic. For now,
however, let’s do it ourselves. As we’ll see, a List
is an extensible collection of Object
s. 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 the
add()
method, which accepts an
object. 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
As we alluded to earlier, allowing Java to do this for us makes the code 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 the next section on autoboxing and unboxing of primitive values.
The Java 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 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
.
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 allowing Java
to box and unbox primitives in performance-critical areas 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
, as well as “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
);
// This object equality is 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.
As we mentioned earlier, Java methods may have
variable-length argument lists or “varargs” that
allow them to take any number of arguments when invoked. The most common
example usage of varargs is for the 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 String
s.
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.
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
.
(
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
(
Object
arg
)
{
...
}
void
(
String
arg
)
{
...
}
void
(
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 figure
out 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 for this 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. In practice, this distinction will not usually be relevant to you, as you will likely recompile all of the necessary classes at the same time. We’ll talk about method overriding later in the chapter.
[13] 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, 4th Edition 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.