5.1
Methods, Calls, Recursion, and Overloading
Learn methods from the first call to overload resolution, with the call stack connecting every step.
Previously on
You can store values, make decisions, repeat work, and process arrays. Most of that work still lives inside main.
This lesson gives code a reusable boundary. By the end, you will be able to define a method, trace its data, and explain every active call.
The problem
This program prints a greeting twice:
public class MethodDemo {
public static void main(String[] args) {
System.out.println("Welcome to the registry");
System.out.println("Choose an operation");
System.out.println("Welcome to the registry");
System.out.println("Choose an operation");
}
}The duplicate lines are a small maintenance problem. They also hide an idea that deserves a name: show the menu.
A method groups statements behind a name. Calling that name runs the grouped work.
You have already called methods. println is one. The new step is defining your own.
The idea
Define and call a method
public class MethodDemo {
static void showMenu() {
System.out.println("Welcome to the registry");
System.out.println("Choose an operation");
}
public static void main(String[] args) {
showMenu();
showMenu();
}
}The method belongs inside the class but outside main. Its declaration has four parts:
static void showMenu ()
modifier result name parametersstatic lets this early program call the method without creating an object. Instance methods arrive with classes and objects in Phase VI.
void means the call produces no result value. showMenu is the method name. Empty parentheses mean it accepts no arguments.
The line showMenu(); is a method call. Execution enters the method body, runs its statements, then continues after the call.
Add one parameter
Hard-coded methods can repeat work, but they cannot adapt. A parameter lets the caller provide a value.
static void greet(String name) {
System.out.println("Hello, " + name);
}
public static void main(String[] args) {
greet("Asha");
greet("Kabir");
}Expected output:
Hello, Asha
Hello, KabirThe relationship is exact:
"Asha"is an argument in the call.nameis a parameter in the declaration.- The parameter is a local variable available only during that call.
Each call gets its own name. One call does not overwrite the parameter of another finished call.
Use several parameters
static void printScore(String name, int score) {
System.out.println(name + ": " + score);
}
printScore("Asha", 91);Arguments match parameters by position. The first argument supplies name; the second supplies score.
The count and types must be compatible. printScore(91, "Asha") fails because an int cannot supply the String parameter.
Return a result
A method can calculate a value and give it back to its caller.
static int larger(int a, int b) {
if (a >= b) {
return a;
}
return b;
}The declared return type is int. Every normal path through this method returns an int value.
The call itself is an expression of type int:
int winner = larger(12, 19);
System.out.println(larger(4, 7));
int total = larger(3, 8) + 10;In each line, Java evaluates the call first. The returned value then takes the call’s place in the surrounding expression.
return has two effects. It supplies the result, and it ends the current method call immediately.
static boolean isPositive(int value) {
if (value > 0) {
return true;
// code here would be unreachable
}
return false;
}A void method may use return; with no value to leave early.
static void printPositive(int value) {
if (value <= 0) {
return;
}
System.out.println(value);
}Extract a complete piece of work
The repeated maximum loop can become one method:
static int highestIn(int[] values) {
if (values.length == 0) {
throw new IllegalArgumentException("values must not be empty");
}
int highest = values[0];
for (int i = 1; i < values.length; i++) {
if (values[i] > highest) {
highest = values[i];
}
}
return highest;
}Now highestIn(scores) tells the reader what the loop accomplishes. It also keeps the empty-array rule next to the implementation that depends on it.
Under the hood
Going deeperArguments are evaluated before the call
Consider this program:
static int announce(int value) {
System.out.println("evaluated " + value);
return value;
}
static void show(int left, int right) {
System.out.println(left + right);
}
public static void main(String[] args) {
show(announce(2), announce(5));
}Java evaluates argument expressions from left to right. The output is:
evaluated 2
evaluated 5
7Only after both argument values exist does show begin.
Java always passes values
For a primitive, the value is the primitive data:
static void change(int number) {
number = 99;
}
int original = 10;
change(original);
System.out.println(original); // 10The parameter receives a copy of 10. Assigning 99 changes only the parameter.
For an array or object, the value is a reference:
static void changeFirst(int[] numbers) {
numbers[0] = 99;
}
int[] original = {10, 20};
changeFirst(original);
System.out.println(original[0]); // 99The parameter receives a copy of the reference. Both references reach the same array, so mutation is visible through either one.
Reassigning the parameter is different:
static void replace(int[] numbers) {
numbers = new int[] {99, 100};
}
int[] original = {10, 20};
replace(original);
System.out.println(original[0]); // 10The parameter now points elsewhere. The caller’s variable still holds its original reference.
Java is therefore pass-by-value in both examples. The type of value differs.
Local variables have method scope
Parameters and variables declared inside a method are local to that invocation.
static int square(int value) {
int result = value * value;
return result;
}Neither value nor result can be named from main. A later call gets new locals, even if an earlier call used the same values.
Local variables do not receive automatic default values. A local must definitely be assigned before Java lets you read it.
static int choose(boolean first) {
int result;
if (first) {
result = 10;
}
return result; // compile-time error: result may be uninitialised
}The compiler examines paths, not your intention. There is a path where first is false and no assignment occurs.
Every active call has a stack frame
The call stack records methods that have started but not finished. A call adds a frame. Returning removes that frame.
static int doubleValue(int n) {
return n * 2;
}
static int addThenDouble(int a, int b) {
int sum = a + b;
return doubleValue(sum);
}
public static void main(String[] args) {
int answer = addThenDouble(3, 4);
}At the deepest point, the stack is:
top doubleValue: n = 7
addThenDouble: a = 3, b = 4, sum = 7
bottom maindoubleValue returns 14, so its frame is removed. addThenDouble then returns 14, and its frame is removed.
A frame holds bookkeeping needed for that invocation. This includes parameters, local values, operand data, and a return point in the JVM model.
This model explains stack traces. The top entry is where the failure occurred. Lower entries show the unfinished callers that led there.
Recursion is an ordinary method calling itself
static int factorial(int n) {
if (n < 0) {
throw new IllegalArgumentException("n must be non-negative");
}
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}A correct recursive method needs two properties:
- A base case that returns without another recursive call.
- A recursive step that moves the input toward that base case.
Trace factorial(4) before running it:
factorial(4) waits for 4 * factorial(3)
factorial(3) waits for 3 * factorial(2)
factorial(2) waits for 2 * factorial(1)
factorial(1) returns 1
factorial(2) returns 2
factorial(3) returns 6
factorial(4) returns 24The calls grow the stack on the way down. Returns unwind it on the way back.
If the input never reaches a base case, each unfinished call adds another frame. Eventually the thread cannot create another frame and throws StackOverflowError.
Java does not promise tail-call elimination. A recursive solution that may be extremely deep should usually become a loop or use an explicit data structure.
Overloading gives one name to several parameter lists
These methods are overloads:
static int area(int side) {
return side * side;
}
static int area(int width, int height) {
return width * height;
}
static double area(double radius) {
return Math.PI * radius * radius;
}An overload must have a different parameter list. The method’s signature consists of its name and parameter types.
Parameter names and return types do not distinguish overloads:
static int parse(String text) { return 1; }
static double parse(String value) { return 1.0; } // duplicate signatureThe compiler chooses an overload using the compile-time types of the argument expressions. The return destination does not choose it.
A practical overload-resolution sequence
For a fixed call, use this sequence:
- Look for applicable fixed-arity methods under strict invocation. Exact matches and primitive widening are available here.
- If none apply, allow loose invocation conversions such as boxing and unboxing.
- If none apply, consider variable-arity methods.
- If several methods survive one phase, choose the most specific applicable method.
- If there is no unique most specific method, the call is ambiguous.
static void pick(long value) { System.out.println("long"); }
static void pick(Integer value) { System.out.println("Integer"); }
static void pick(int... values) { System.out.println("varargs"); }
pick(1); // longThe literal 1 has type int. Primitive widening to long is available in the first phase. Boxing is not considered because an applicable method already exists.
Varargs are syntax for receiving an array:
static int sum(int... values) {
int total = 0;
for (int value : values) {
total += value;
}
return total;
}
sum();
sum(4);
sum(4, 5, 6);Inside sum, values has type int[]. At a variable-arity call site, the compiler arranges the supplied values into an array.
Declared types can change overload selection
static void describe(Object value) { System.out.println("object"); }
static void describe(String value) { System.out.println("string"); }
String text = "hi";
Object general = text;
describe(text); // string
describe(general); // objectBoth variables refer to the same object. Overloading is still chosen from their compile-time types.
Overriding behaves differently because it uses the object’s run-time class. Phase VII separates those mechanisms in full.
null can expose ambiguity:
static void load(String value) { }
static void load(Integer value) { }
// load(null); // ambiguous: neither parameter type is more specific than the otherThe compiler refuses to guess.
What it costs
Small methods improve naming, reuse, testing, and stack traces. Fragmenting one idea across many tiny methods can make the path harder to follow.
Overloading is useful when operations have one meaning across closely related inputs. Unrelated behaviour behind one name makes calls harder to predict.
Recursion can mirror tree and divide-and-conquer problems well. A loop is clearer for many linear repetitions and does not consume one frame per step.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
What is the difference between an argument and a parameter?
Show the answer
An argument is an expression at the call site. A parameter is a local variable declared by the method. Java evaluates each argument, copies the resulting value into its matching parameter, and starts the method body.
A method receives an array and changes one element. Java is pass-by-value, so how can the caller observe the change?
Show the answer
The copied value is a reference. The caller and parameter hold separate copies of the same reference, so both can reach one array object. Reassigning the parameter changes only its local copy. Mutating the shared array changes the object both references reach.
Why does recursion without a reachable base case normally end in StackOverflowError?
Show the answer
Every unfinished call needs a stack frame. A recursive call pushes another frame before the current call can return. If the base case is never reached, frames keep accumulating until that thread's stack has no room for another call.
Given pick(long x) and pick(Integer x), which overload handles pick(1)?
Show the answer
pick(long)handles it. Strict fixed-arity matching allows primitive widening frominttolong. The compiler only considers boxing during a later loose-invocation phase if strict matching found no applicable method.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
5 exercises115 pointsabout 135 minutes
Extract Three Loops Into One Method
ex-5-1-aThree copies of one idea. Pull it out into a method, then add two more like it.
This is the most common refactoring you will ever do, and doing it once on purpose is worth more than reading about it.
Watch the average. Both the total and the length are ints, so the division is int division and your decimal disappears. That is the same bug from Section 2.3, showing up in real code where it is much harder to see.
Then answer the last question properly. Removing repetition is the obvious gain. There is a second one, and it is the reason this refactoring is worth doing even when the code appears only once.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Extract {
static int highestIn(int[] values)
static int lowestIn(int[] values)
static double averageOf(int[] values)
static int rangeOf(int[] values)
}All four work on any int array, including one that is entirely negative. The average must not overflow on large values.
What your program must do
- Replace three copied loops with one method
- Add lowest, average and range
- Make it work on an array of negatives
- Say what you would have to change if the same bug were in all three copies
public class Extract {
// One method that works for any array, replacing all three copied loops.
static int highestIn(int[] values) { return 0; } // TODO
static int lowestIn(int[] values) { return 0; } // TODO
static double averageOf(int[] values) { return 0; } // TODO
static int rangeOf(int[] values) { return 0; } // TODO: highest minus lowest
public static void main(String[] args) {
int[] scores = {78, 65, 91, 54, 88};
int[] temperatures = {31, 28, 35, 22, 30};
int[] distances = {120, 340, 90, 500, 210};
// TODO: call one method three times instead of writing three loops
}
}
Hint 1
highestIn at values[0]. Starting at 0 gives the wrong answer for an array where everything is negative.Hint 2
long before dividing. Two values near Integer.MAX_VALUE will overflow an int total before you get to the division.Hint 3almost the answer
Draw the Stack
ex-5-1-bDraw the call stack by hand, then check yourself against a real one.
The program deliberately prints a stack trace from the deepest point, using an exception it never throws. Read that trace and match every line to a box in your drawing. They should correspond exactly, and seeing that correspondence once changes how you read every stack trace afterwards.
Predict the result before running. It is simple arithmetic, and the point is to trace the values through the frames rather than through the source.
Finish by explaining why three variables named n do not collide. If you can say that clearly, you understand what a frame is.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Frames {
static List<String> currentFrames()
static int depthNow()
static List<String> level1(int n)
static List<String> level2(int n)
static List<String> level3(int n)
static int level1Value(int n)
static int level2Value(int n)
static int level3Value(int n)
}currentFrames returns the names of the methods on the stack right now, innermost first, filtered to this class only. The three value methods do the arithmetic: level1 adds 10 and calls level2, which doubles and calls level3, which adds one.
What your program must do
- Print the stack from the deepest method and draw it on paper
- Show that the frames disappear as the methods return
- Predict the arithmetic result before running
- Say what each frame is holding
import java.util.*;
public class Frames {
// The names of OUR methods on the stack right now, innermost first.
static List<String> currentFrames() { return List.of(); } // TODO
static int depthNow() { return 0; } // TODO
// These three just report the stack from the bottom of the chain.
static List<String> level3(int n) { return List.of(); } // TODO
static List<String> level2(int n) { return List.of(); } // TODO: call level3 with n * 2
static List<String> level1(int n) { return List.of(); } // TODO: call level2 with n + 10
// The same chain, doing arithmetic. Predict level1Value(5) before running.
static int level3Value(int n) { return 0; } // TODO: n + 1
static int level2Value(int n) { return 0; } // TODO: level3Value(n * 2)
static int level1Value(int n) { return 0; } // TODO: level2Value(n + 10)
public static void main(String[] args) {
// TODO: print the frames from the bottom of the chain, and draw the stack
// TODO: predict level1Value(5) BEFORE running it
}
}
Hint 1
new Exception().getStackTrace() gives you the frames without throwing anything. Filter to your own class or the JUnit machinery drowns it.Hint 2
main is at the bottom.Hint 3almost the answer
n. Three frames means three different values of a variable with one name, which is why the arithmetic threads through the way it does.Recursion, and Where It Runs Out
ex-5-1-cThree recursive methods, then a deliberate crash.
For each one, write down the base case before you write any code. That is the part people skip, and skipping it is what produces the crash at the bottom of this file.
Then run runaway on purpose and find out how deep your machine actually goes. The number will surprise you: not millions, and not a hundred. Somewhere in the tens of thousands, which is close enough to real problem sizes to matter.
Finish by rewriting factorial as a loop and comparing. For factorial the loop is at least as clear and has no depth limit, which is worth noticing. Recursion earns its cost on trees and nested structures, not on counting.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Recurse {
static long factorial(int n)
static int sumDigits(int n)
static boolean isPalindrome(String s, int left, int right)
static long factorialLoop(int n)
static int depthReached()
}depthReached deliberately recurses until the stack runs out and returns how deep it got, catching the StackOverflowError itself. sumDigits must handle a negative input.
What your program must do
- Write all three recursive methods with a correct base case
- Write factorial again as a loop and confirm they agree
- Find how deep recursion goes on your machine before it runs out
- Say what each frame costs and why the loop version has no limit
public class Recurse {
static long factorial(int n) { return 0; } // TODO
static int sumDigits(int n) { return 0; } // TODO: sumDigits(1234) is 10
static boolean isPalindrome(String s, int left, int right) { return false; } // TODO
// The same job with a loop, so nothing goes on the stack.
static long factorialLoop(int n) { return 0; } // TODO
// Recurse until the stack runs out. Catch the error and report the depth.
static int depthReached() { return 0; } // TODO
public static void main(String[] args) {
// TODO: check all three against known answers
// TODO: find how deep this machine lets you go, then try -Xss256k
}
}
Hint 1
Hint 2
isPalindrome stops when left >= right, which covers both meeting in the middle and crossing over on an even length.Hint 3almost the answer
StackOverflowError inside the recursion itself and return the depth. The number is a fact about your stack size, so -Xss changes it, which tells you what the limit is made of.Which Overload Wins
ex-5-1-dSeven calls. Predict all seven before you run anything, and write down a reason for each.
Do not guess by which one “looks closest”. Work through strict fixed arity, loose fixed arity, then variable arity. Stop at the first phase with applicable methods, then choose the most specific one.
Two of these are genuinely surprising. f('a') has no char version and still resolves without complaint. And g(1) picks the long version over the Integer version, which most people get wrong.
Finish by breaking it on purpose. Add a method that makes one of the calls ambiguous, and read the compiler error. Seeing the compiler admit it cannot choose is the clearest proof that these rules are real and not folklore.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class Which {
static String f(int x)
static String f(long x)
static String f(double x)
static String f(Integer x)
static String f(Object x)
static String f(int... x)
static String g(long x)
static String g(Integer x)
static String h(Integer x)
static String h(int... x)
static String passThatWins(String call)
}Each overload returns its own parameter type as a string, so the checks can see which one was chosen. passThatWins takes a call written as text and names the pass that decided it.
What your program must do
- Predict which overload wins for all seven calls before running
- Say which invocation phase decided each one
- Explain why g(1) does not pick Integer
- Explain why varargs is genuinely last
public class Which {
// Each one returns its own parameter type, so you can see which was picked.
static String f(int x) { return ""; } // TODO
static String f(long x) { return ""; } // TODO
static String f(double x) { return ""; } // TODO
static String f(Integer x) { return ""; } // TODO
static String f(Object x) { return ""; } // TODO
static String f(int... x) { return ""; } // TODO
static String g(long x) { return ""; } // TODO
static String g(Integer x) { return ""; } // TODO
static String h(Integer x) { return ""; } // TODO
static String h(int... x) { return ""; } // TODO
// Which invocation phase decided this call?
static String passThatWins(String call) { return ""; } // TODO
public static void main(String[] args) {
// PREDICT each of these before running:
// f((byte) 1) f(1) f(1L) f('a') f(1.5f) g(1) h(1)
}
}
Hint 1
Hint 2
g(1) picks long because an int widens to a long in pass one, so pass two never runs and Integer never gets considered.Hint 3almost the answer
h(1) picks Integer because boxing is pass two and varargs is pass three. Varargs is the last resort, which is why adding one to a class can quietly change which method a call goes to.The Registry Grows Methods
ex-5-1-eYour Registry has been one long main for two phases. Break it into named methods.
Every method here is small. The exercise is not the code, it is the naming and the structure. When you are done, main should read like a description of what the program does, and each method should do one thing you could describe in a sentence.
Two things to get right.
Use .equals() for the name comparison. == will compile, run, and silently return -1 for a name that is clearly in the array. That is Section 3.2 coming back at exactly the moment you stopped thinking about it.
Make the overload share its implementation. Two versions of averageReadiness should not contain the logic twice. The no-argument version should be one line that calls the other. Two copies of anything is two things that can drift apart.
What to write
These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.
class RegistryOps {
static String[] names()
static int[] ids()
static double[] readiness()
static boolean[] active()
static int findById(int[] ids, int id)
static int findByName(String[] names, String name)
static double averageReadiness(double[] readiness)
static int countDeployable(double[] readiness, boolean[] active)
static String describe(String[] names, double[] readiness, int index)
static String describe(String[] names, int[] ids, double[] readiness, int id)
}The two describe methods are overloads: one takes an index, the other an id. Both return "Name at Readiness" or "unknown". Finders return -1 for a miss.
What your program must do
- Write finders that return an index, and -1 for a miss
- Write two describe overloads that agree for the same unit
- Handle a unit that is not in the roster
- Say why the id overload can be written in terms of the index one
public class RegistryOps {
static String[] names() { return new String[]{"Atlas", "Beacon", "Cipher", "Drift", "Ember"}; }
static int[] ids() { return new int[]{101, 102, 103, 104, 105}; }
static double[] readiness() { return new double[]{88.5, 42.0, 95.5, 61.0, 73.5}; }
static boolean[] active() { return new boolean[]{true, true, false, true, true}; }
static int findById(int[] ids, int id) { return -1; } // TODO
static int findByName(String[] names, String name) { return -1; } // TODO
static double averageReadiness(double[] readiness) { return 0; } // TODO
static int countDeployable(double[] readiness, boolean[] active) { return 0; } // TODO
// Two overloads: one takes an index, one takes an id.
// Both return "Name at Readiness", or "unknown".
static String describe(String[] names, double[] readiness, int index) { return ""; } // TODO
static String describe(String[] names, int[] ids, double[] readiness, int id) { return ""; } // TODO
public static void main(String[] args) {
// TODO: use both overloads and confirm they agree for the same unit
}
}
Hint 1
Hint 2
equals. == works for the literals in the array and fails the moment a name comes from input, which is Section 3.2.Hint 3almost the answer
After the credits
You should now be able to trace a call using five questions:
- Which declaration did the compiler select?
- In what order are the arguments evaluated?
- Which values are copied into the parameters?
- What frame is added to the stack?
- Which value, if any, replaces the call when it returns?
Objects add a receiver to this model. The call mechanics remain.
Threads you opened in this section
- The call stackThe reference sits on the stack. The object sits on the heap.6.3 - Objects in Memory: Size, Copying, Passing
- The call stackA stack trace is this pile, printed out. StackOverflowError is it overflowing.Phase XII. When Things Go Wrong
- The call stackEvery thread gets its own stack. That is most of what a thread is.Phase XIV. Concurrency
- RecursionTreeMap is a tree, and trees are walked with recursion.Phase X. The Collections Framework
- Method overloadingOverloading vs overriding. Compile time vs run time. The classic mix-up.7.2 - Abstraction, Polymorphism, Abstract Classes, and Interfaces
- Method overloadingremove(1) and remove(Integer.valueOf(1)) do completely different things.7.3 - Wrapper Types, Autoboxing, and POJOs
Method overloading will return in 6.2 - Constructors, Chaining, `this`