A method is a named block of code
Think about System.out.println. You call it by name, give it something to display, and it does its job. You have used it from your first program without needing to know how the characters reach the screen. That is the idea of a method: a named block of code that only runs when it is called.
A block of code is any section of code enclosed in braces { }. A method is a block with a name. Methods exist to avoid repeating code and to make programs readable: you write the work once, give it a name, and call it wherever it is needed.
Procedural abstraction is what lets a programmer use a method by knowing what the method does, even if they do not know how the method was written. You have been doing this since Topic 1.7; here we look at how methods are described.
The anatomy of a method header
The first line of a method is its header. Here is the header of a method that averages two whole numbers:
- The return type (
double) is the type of the value the method gives back. If the method gives back nothing, the return type isvoid. - The name (
average) is what you write to call it. - The parameters (
int a, int b) are variables declared in the header of the method. They can be used inside the body of the method, and they allow values (arguments) to be passed to the method when it is called.
A method signature for a method with parameters consists of the method name and the ordered list of parameter types. For average it is average(int, int). A method without parameters has a signature made of its name and an empty parameter list, such as second().
The words public and static also appear in the header. For now, treat public static as part of the pattern; Topic 1.10 explains static, and Unit 3 explains public and when methods are not static.
Calling a method
A complete program with several methods shows the rules:
public class Signatures {
public static void greet(String name) {
System.out.println("Hello, " + name);
}
public static int square(int n) {
return n * n;
}
public static double average(int a, int b) {
return (a + b) / 2.0;
}
public static void main(String[] args) {
greet("Ada");
int s = square(6);
double avg = average(3, 4);
System.out.println(s + " " + avg);
System.out.println(square(2 + 3));
}
}
Hello, Ada
36 3.5
25
Void and non-void methods
- A void method does not have a return value, so it is not called as part of an expression.
greet("Ada");stands alone as a statement. Writingint r = greet("Ada");is a compile-time error. - A non-void method returns a value of the type named in its header. To use the returned value, you must store it in a variable (
int s = square(6);) or use it as part of an expression (square(2 + 3)insideprintln, oraverage(1, 2) + average(3, 4)). You may also call it as a statement and discard the value, as insquare(4);, but then the call has no visible effect.
Arguments must match parameters
An argument is a value passed into a method when it is called. The arguments must be compatible in number and order with the types in the parameter list. Passing square("3") fails, because a String is not an int. Passing square(2, 3) fails, because there are two arguments and only one parameter. Passing show(3, "a") to a method declared as show(String label, int value) fails because the order is wrong. The compiler reports all of these.
An int may be passed where a double is expected (it is converted automatically), but not the other way round.
Call by value
When calling methods, arguments are passed using call by value. Call by value initializes the parameters with copies of the arguments. So a method that changes a primitive parameter changes only its own copy:
public class ByValue {
public static void change(int x) {
x = x + 10;
System.out.println("inside: " + x);
}
public static void main(String[] args) {
int n = 5;
change(n);
System.out.println("outside: " + n);
}
}
inside: 15
outside: 5
The parameter x started as a copy of n’s value, 5. Adding 10 to the copy left n untouched.
The flow of control
A method call interrupts the sequential execution of statements. The program first executes the statements in the method before continuing. Once the last statement in the method has been executed, or a return statement is executed, the flow of control returns to the point immediately following where the method was called.
public class Flow {
public static void first() {
System.out.println("first start");
second();
System.out.println("first end");
}
public static void second() {
System.out.println("second");
}
public static void main(String[] args) {
System.out.println("main start");
first();
System.out.println("main end");
}
}
main start
first start
second
first end
main end
Overloading
Methods are said to be overloaded when there are multiple methods with the same name but different signatures. The compiler chooses the version whose parameter types match the arguments you supply.
public class Overload {
public static int add(int a, int b) {
return a + b;
}
public static double add(double a, double b) {
return a + b;
}
public static int add(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
System.out.println(add(2, 3));
System.out.println(add(2.5, 1));
System.out.println(add(2, 3.5));
System.out.println(add(1, 2, 3));
}
}
5
3.5
5.5
6
add(2, 3) matches add(int, int) exactly and gives an int. add(2.5, 1) has a double argument, so it can only use add(double, double); the 1 is widened to 1.0. The three-argument call matches the third method. Because the signature is only the name and the parameter types, two methods may not differ only in their return type: the compiler cannot tell which one a call means, and reports that the method is already defined.
Checking a call the way the compiler does
When you meet a method call in code, you can check it in the same order the compiler does. This turns a vague feeling of “that looks right” into a short, reliable procedure.
- Find the candidates. List every method with that name. If there is only one, there is nothing to choose; if there are several, the class has overloaded the name.
- Count the arguments. Discard any candidate whose number of parameters differs from the number of arguments. In the call
add(1, 2, 3)only the three-parameter method survives. - Work out the type of each argument expression. The argument
2 + 3is anint;2 + 3.5is adouble;"3"is aString. Remember that an argument can itself contain method calls, which are evaluated first, from left to right, before the outer call happens. - Compare types position by position. An argument fits a parameter of the same type, and an
intargument also fits adoubleparameter. Nothing else fits. If more than one candidate still fits, the one that matches most exactly is chosen. - Check how the result is used. If the method is void, the call must be a statement on its own. If it returns a type, the surrounding code must accept that type.
Failing at step 2 or 4 gives the “cannot be applied to given types” error; failing at step 5 gives the “incompatible types” error. If nothing in the class has that name, the compiler says it cannot find the symbol.
The same five steps also tell you what a call means at run time. The parameters are created and initialized with copies of the arguments; the body runs; a return statement hands a value back, which replaces the whole call in the surrounding expression; and execution resumes straight after. When you trace by hand, it helps to draw a small box for each method call, showing its parameters and any local variables, and to cross the box out when the method returns. The boxes make it obvious why a parameter named n inside a method is a different variable from an n in the caller.
Worked example 1: Trace a program with calls
Task. State the output.
public class Trace19 {
public static int triple(int n) {
n = n * 3;
return n;
}
public static void show(String label, int value) {
System.out.println(label + ": " + value);
}
public static void main(String[] args) {
int x = 4;
int y = triple(x);
show("x", x);
show("y", y);
show("sum", triple(x) + triple(1));
}
}
Step 1, start in main. Execution begins at main. x is 4.
Step 2, int y = triple(x);. The parameter n is initialized with a copy of x (4). Inside the method, n becomes 12 and return n; sends 12 back to the point where the call occurred. The value 12 is stored in y. x is still 4, because only the copy changed.
Step 3, the first two show calls. show("x", x) is a void method, called as a statement: label is "x", value is 4. It prints x: 4. Then show("y", y) prints y: 12.
Step 4, the last call. The second argument is an expression: triple(x) + triple(1). It is evaluated first, left to right: triple(x) is 12 and triple(1) is 3. The sum 15 is passed as value, so sum: 15 is printed.
| Statement | x | y | Printed |
|---|---|---|---|
int x = 4; |
4 | - | |
int y = triple(x); |
4 | 12 | |
show("x", x); |
4 | 12 | x: 4 |
show("y", y); |
4 | 12 | y: 12 |
show("sum", triple(x) + triple(1)); |
4 | 12 | sum: 15 |
Output.
x: 4
y: 12
sum: 15
Worked example 2: Which calls match which signature?
Task. A class declares four methods. You may use only their signatures.
static int twice(int n) returns 2 * n
static double twice(double n) returns 2 * n
static int sum(int a, int b, int c) returns a + b + c
static void report(String label, int value) prints label=value
For each call, say whether it compiles and give the value, or the reason it fails:
(a) twice(8) (b) twice(2.5) (c) twice(3) + twice(0.5) (d) sum(1, 2) (e) int v = report("a", 3); (f) report(3, "a");
(a) One int argument matches twice(int) exactly. It compiles and returns the int 16.
(b) 2.5 is a double, so only twice(double) fits. It returns the double 5.0.
(c) twice(3) uses the int version and returns 6. twice(0.5) uses the double version and returns 1.0. In the sum, 6 + 1.0 has a double operand, so the value is the double 7.0.
(d) Does not compile: sum needs three arguments and only two are supplied.
(e) Does not compile: report is a void method, which has no return value to store in v.
(f) Does not compile: the parameters are (String, int), so the arguments must be in that order, but here they are (int, String).
The program that compiles the valid calls prints 16, 5.0 and 7.0 for (a) to (c).
public class Check {
public static int twice(int n) {
return 2 * n;
}
public static double twice(double n) {
return 2 * n;
}
public static void main(String[] args) {
System.out.println(twice(8));
System.out.println(twice(2.5));
System.out.println(twice(3) + twice(0.5));
}
}
16
5.0
7.0
Common misconceptions
- “The return type is part of the signature.” It is not. The signature is the name and the ordered list of parameter types.
- “Parameter names are part of the signature.” They are not.
- “A method changes the variable I pass in.” For primitive arguments the parameter is a copy (call by value).
- “A method runs when the program starts.” A method runs only when it is called (apart from
main, where execution begins). - “I can use a void method in an expression.” It has no value.
- “If I ignore a non-void result the compiler complains.” It does not; the value is discarded.
- “Overloading means any two methods with similar names.” The names must be the same and the signatures different.
- “After a call the program continues where the method ends.” It continues at the point immediately after the call.
Where this leads
Try the Topic 1.9 practice questions, recap with the revision notes and tick off the checklist. The next topic, 1.10 Calling Class Methods, explains the static keyword and how to call a method that belongs to a class rather than to an object.
Quick questions
What exactly is in a method signature?
The method name and the ordered list of parameter types. For average(int a, int b) the signature is average(int, int). The return type and the parameter names are not part of the signature, which is why two methods cannot differ only in return type.
What is the difference between a parameter and an argument?
A parameter is a variable declared in the method's header, such as int n in square(int n). An argument is the value you pass when you call the method, such as 6 in square(6). Each parameter is initialized with a copy of the matching argument.
What does call by value mean?
The parameters are initialized with copies of the arguments. If the method changes a primitive parameter, the variable in the calling code is not affected.
Can I call a non-void method as a statement and ignore the result?
Yes. Calling square(4); on its own compiles; the returned value is just discarded. To use the result you must store it in a variable or use the call as part of an expression.