A method is a named behaviour
If a class’s instance variables are what an object knows, its methods are what it does. You have been calling other people’s methods since Unit 1, such as substring and Math.abs. In this topic you write your own, which is what the second free-response question on the exam asks you to do (alongside the class header, private instance variables and constructors).
Every method has a header and a body. The header says three things: the method’s access (public or private), its return type (or void), and its name with the parameter list. The body is the code that carries out the behaviour.
Void and non-void methods
- A void method does not return a value. Its header has the keyword
voidwhere the return type would be. It is called for its effect, such as changing an instance variable or printing. - A non-void method returns a single value. Its header has the return type, such as
int,booleanorString, in place ofvoid. It is called for its result, which can be stored, printed or used in an expression.
In a non-void method, a return expression compatible with the return type is evaluated, and the value is returned. This is called return by value: the caller receives the value that was computed.
public int getCoins() // non-void: returns an int
public void addCoins(int amount) // void: returns nothing
public boolean spend(int amount) // non-void: returns a boolean
What return does to the flow of control
The return keyword returns the flow of control to the point where the method or constructor was called. This has consequences you must be able to trace:
- Code after a
returnon the same path never executes. In fact the compiler refuses it as an unreachable statement. - A
returninside a selection or a loop halts that statement and exits the whole method. Execution does not go on to the next iteration, and does not finish the loop. - A non-void method must return a value on every path. If the compiler cannot see that every path ends in a
return, it reportsmissing return statement.
A void method may also use a bare return; to leave early, though that is less common.
Accessors and mutators
Because instance variables are private, other classes need public methods to read and change them.
- An accessor method lets objects of other classes obtain a copy of the value of an instance variable (or class variable). It is a non-void method:
getCoins(). - A mutator (modifier) method changes the values of instance variables (or class variables). It is often void:
addCoins(int amount). A mutator may also return a value, asspenddoes when it reports success.
Parameters of primitive type
Methods with parameters receive values through them and use those values to do their job. When an argument is a primitive value (int, double, boolean), the parameter is initialised with a copy of that value. Changes to the parameter have no effect on the corresponding argument.
Preconditions: what must be true for a method to work
A method works as described only if certain initial conditions hold. These are its preconditions. For average(int total, int count) that returns total / count, a sensible precondition is count > 0. If a caller passes count of 0, an int division by zero throws ArithmeticException with the message / by zero. When you read a specification, mark the preconditions (for example, that an object reference is not null); they tell you what you do not have to check and what the caller is responsible for.
Worked example 1: writing and tracing methods from a specification
Task. A Wallet stores a whole number of coins. Write: an accessor getCoins; a mutator addCoins that adds an amount; a method spend that removes an amount only if the wallet holds at least that many, returning whether it did; and coinsWithBonus(int percent) that returns the number of coins the wallet would hold with that percentage bonus added (integer arithmetic, the wallet is not changed).
Step 1: choose each header. getCoins returns an int, so public int getCoins(). addCoins just changes state: public void addCoins(int amount). spend must report success: public boolean spend(int amount). coinsWithBonus returns an int and must not change coins: public int coinsWithBonus(int percent).
Step 2: write the bodies.
public class Wallet
{
private int coins;
public Wallet(int startingCoins)
{
coins = startingCoins;
}
public int getCoins()
{
return coins;
}
public void addCoins(int amount)
{
coins += amount;
}
public boolean spend(int amount)
{
if (amount > coins)
{
return false;
}
coins -= amount;
return true;
}
public int coinsWithBonus(int percent)
{
return coins + coins * percent / 100;
}
}
Step 3: trace. Start with Wallet w = new Wallet(40);.
w.addCoins(10);
System.out.println(w.spend(70));
System.out.println(w.spend(30));
System.out.println(w.getCoins());
System.out.println(w.coinsWithBonus(15));
| Call | coins before |
Test or calculation | coins after |
Output |
|---|---|---|---|---|
addCoins(10) |
40 | 40 + 10 | 50 | |
spend(70) |
50 | 70 > 50 is true, so return false |
50 | false |
spend(30) |
50 | 30 > 50 is false; subtract; return true |
20 | true |
getCoins() |
20 | 20 | 20 |
|
coinsWithBonus(15) |
20 | 20 + 20 × 15 ÷ 100 = 20 + 3 | 20 | 23 |
The output of the compiled program was false, true, 20, 23.
Check. In spend, the first return false; ends the method, so the subtraction never runs on the failing path. Without the early return, a failing call would have changed the state. In coinsWithBonus, the expression coins * percent / 100 is evaluated as (20 * 15) / 100 = 300 / 100 = 3, using integer division, and coins itself was not changed, so getCoins() would still be 20.
Worked example 2: primitive parameters and returns inside loops
Part A: does the argument change?
public class Helper
{
public void addTen(int x)
{
x = x + 10;
}
public int addTenAndReturn(int x)
{
x = x + 10;
return x;
}
}
Helper h = new Helper();
int n = 5;
h.addTen(n);
System.out.println(n);
n = h.addTenAndReturn(n);
System.out.println(n);
addTen(n) gets a copy of 5. Its x = x + 10; makes the copy 15, and nothing is returned, so n stays 5 and the first output is 5. addTenAndReturn(n) also works on a copy, but it returns it with return x;. The assignment n = ... stores the returned 15 into n, so the second output is 15. The program printed 5 then 15. The only way for a method to give a primitive result to the caller is to return it.
Part B: return inside a loop.
public class Checks
{
/** Precondition: n >= 0. */
public boolean isPrime(int n)
{
if (n < 2)
{
return false;
}
for (int d = 2; d * d <= n; d++)
{
if (n % d == 0)
{
return false;
}
}
return true;
}
public String grade(int mark)
{
if (mark >= 70)
{
return "A";
}
if (mark >= 50)
{
return "B";
}
return "C";
}
}
Consider isPrime. Trace three calls.
| Call | Early test | Loop trace (d, d × d <= n?, n % d) | Returns |
|---|---|---|---|
isPrime(15) |
15 < 2? no | d=2: 4 <= 15, 15 % 2 = 1; d=3: 9 <= 15, 15 % 3 = 0, so return false |
false |
isPrime(13) |
no | d=2: 4 <= 13, 1; d=3: 9 <= 13, 1; d=4: 16 <= 13 is false, loop ends | true |
isPrime(2) |
no | d=2: 4 <= 2 is false, so the loop body never runs | true |
isPrime(1) |
1 < 2: return false |
not reached | false |
The output of isPrime(15) + " " + isPrime(13) + " " + isPrime(2) + " " + isPrime(1) was false true true false.
Check. For isPrime(15), once return false; executed at d = 3, the loop did not continue to d = 4. That is the “return exits the whole method” rule in action. The final return true; is reached only when the loop finishes without finding a divisor.
Reading compile errors in methods
Four mistakes appear in questions asking why code does not compile. All four messages below came from the compiler:
- Missing return on some path. An
if/else ifwith no finalelseand no return after it:missing return statement. Fix: add a finalreturnor make the last branch anelse. - Statement after a return.
return x * 2;followed by aSystem.out.printlnin the same block:unreachable statement. Fix: move or remove the statement. - Wrong type.
public int half(double x) { return x / 2; }:incompatible types: possible lossy conversion from double to int. Fix: change the return type, or cast deliberately. - Returning a value from a void method.
public void show(int x) { return x; }:incompatible types: unexpected return value. Fix: changevoidtoint, or remove the value.
Common misconceptions
- “A method that changes its parameter changes the caller’s variable.” For a primitive argument, the parameter is a copy, so the caller’s variable is unaffected.
- “
returnjust ends the loop.” It ends the whole method. - “A void method can give a result back if I name it well.” A void method returns nothing. If the caller needs a value, the method must be non-void.
- “An accessor should change the object.” An accessor returns a copy of a value and leaves the state unchanged.
- “If my
ifhas areturn, I do not need one after it.” You do, unless every path returns. The compiler checks all paths. - “A call to a non-void method updates the variable automatically.” The returned value is lost unless you store or use it, as in
n = h.addTenAndReturn(n);.
Where this leads
Methods are the heart of the class-design free-response question, and they come back in nearly every later topic. Passing and Returning References of an Object covers what changes when the argument is an object rather than a primitive. First, try the practice questions, review the revision notes and tick the topic checklist. To check your constructors, see Constructors.
Quick questions
What is the difference between an accessor and a mutator?
An accessor lets other classes obtain a copy of the value of an instance variable or class variable, so it is a non-void method. A mutator (modifier) changes the value of an instance variable or class variable and is often void.
If I change a parameter inside a method, does the variable I passed in change?
Not when the argument is a primitive value. The parameter starts as a copy of the value, so assigning to it changes only the copy. (If the argument is an object reference, the method can alter the object it refers to; see Topic 3.6.)
Why does the compiler say missing return statement?
Because a non-void method must return a value on every path through it. If an if or else if might be skipped, add a final return or an else so every path ends in a return.
Can a method have more than one return statement?
Yes. Several return statements are common, for example one in each branch of a selection or one inside a loop. As soon as one executes, the method ends.