Writing your own methods, following the flow of execution as it detours from one to another, passing arguments into parameters, and drawing the stack diagram that shows which variables exist and where. Follows Think Java 2e, Chapter 4 (Methods and Testing), Sections 4.1-4.5, pp. 51-58, cross-referenced against The Java Tutorials — Defining Methods.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 4 · Methods and Testing
Sections 4.1-4.5 · pp. 51-58
Objectives
This lesson follows Think Java 2e, Chapter 4 (Methods and Testing), Sections 4.1-4.5, pp. 51-58. Everything on these slides can be checked against those pages.
1. Define a void method and invoke it from main.
2. Trace the flow of execution through several methods, and say why it is not the order they appear in the file.
3. Give three reasons to write a method rather than putting everything in main.
4. Distinguish an argument from a parameter, and describe parameter passing as an assignment.
5. Explain why a variable in one method is invisible in another, and what 'local variable' means.
6. Draw a stack diagram with a frame per running method, and use it to reason about scope.
Warm-up
You have been calling methods since Chapter 1 without writing one.
Discussion prompt
Name three methods you have already used. For each, say whether it gives you a value back or just does something. What is the difference, in the code you write around it?
Hint: Compare System.out.println(x); with int n = in.nextInt();
Answer:
println and print do something and give nothing back — you call them on a line of their own. nextInt, nextDouble and nextLine return a value, so you have to catch it: int n = in.nextInt();
That distinction is exactly what this chapter formalises. Methods that carry out actions without returning a result are declared void; methods that return something declare the type they return. This lesson covers the first kind, and Lesson 4b covers the second.
Concept
Every program so far has had exactly one method, main. This chapter shows how to organise a program into several — which lets you name a block of statements, avoid repeating yourself, and test parts of a program separately.
Figure (svg): Three boxes giving reasons to write a method: naming a block, removing repetition, and breaking a problem into subproblems
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 4 (Methods and Testing), Sections 4.1-4.5, pp. 51-58 — Chapter 4 opens on printed page 51.
Section
Section 4.1
Concept
Some methods perform a computation and return a result — nextDouble reads input and returns a double. Others, like println, carry out a sequence of actions without returning anything. Java uses the keyword void to define such a method.
public static void newLine() {
System.out.println();
}
public static void main(String[] args) {
System.out.println("First line.");
newLine();
System.out.println("Second line.");
}| part of the header | what it says |
|---|---|
| public | the method can be invoked from other classes |
| static | it belongs to the class rather than to an object — Chapter 10 |
| void | it does not return a result |
| newLine | its name, which you choose |
| () | it takes no parameters |
System.out.println() with no argument displays a blank line — which is the whole job of newLine. The output of this program is First line., a blank line, then Second line.
Notation
You have been reading this header since Chapter 1 without being able to change it. Now every part of it is a decision you make.
Annotate
public — the method can be invoked from other classes. Both newLine and main are public in this example.static — still boilerplate for now, as it was in Chapter 1. It means the method belongs to the class itself rather than to an object; Chapter 10 makes that meaningful.void — this method returns no result, in contrast to something like nextDouble. Lesson 4b replaces this word with a real type.main or a Java keyword.newLine needs no information to do its job.NewLine and the method is newLine — different names, and the capitalisation is what tells a reader which is which.Only the last three parts vary from method to method in this chapter. public static will be on the front of every method you write until Chapter 11.
Worked example
Once a method exists you can call it from another method — including one you also wrote. Build up three blank lines from one.
public class NewLine {
public static void newLine() {
System.out.println();
}
public static void threeLine() {
newLine();
newLine();
newLine();
}
public static void main(String[] args) {
System.out.println("First line.");
threeLine();
System.out.println("Second line.");
}
}| method | what it does | calls |
|---|---|---|
| newLine | displays one blank line | println |
| threeLine | displays three blank lines | newLine, three times |
| main | prints two lines with a gap between | println, threeLine, println |
Write the smallest method first.
Why: newLine does one thing, and it is the thing you will reuse.
Build the larger method out of the smaller one.
Why: threeLine calls newLine three times rather than calling println three times — so if the definition of 'a blank line' ever changes, one place changes.
Call it from main.
Why: threeLine(); on a line of its own, because it is a void method and there is no value to catch.
Note what the class now contains.
Why: NewLine contains three methods: newLine, threeLine and main. A class, for now, is a collection of methods.
Verify: Run it and expect First line., three blank lines, then Second line.
Why: If you got only one blank line, check that threeLine is actually being called rather than newLine — the two names differ by more than their length, and this is exactly the kind of slip a good method name prevents.
Prediction
Read the calls, not the order of the definitions.
public class NewLine {
public static void newLine() {
System.out.println();
}
public static void threeLine() {
newLine();
newLine();
newLine();
}
public static void main(String[] args) {
System.out.println("First line.");
threeLine();
System.out.println("Second line.");
}
}| statement in main | output |
|---|---|
| println("First line.") | First line. |
| threeLine() | three blank lines |
| println("Second line.") | Second line. |
Predict first
How many lines of output are there in total?
Correct: 5
Why: One line of text, then three blank lines from threeLine, then a second line of text — five lines altogether. Blank lines are still lines: System.out.println() with no argument outputs nothing followed by a newline, which is exactly what makes the gap.
Concept
A method's name is the only documentation most readers will look at. The conventions are worth following because the entire Java library follows them.
| convention | example | why |
|---|---|---|
| begins with a lowercase letter | newLine, printTwice | distinguishes it from a class name |
| camel case for later words | printTime, calculateArea | readable without spaces, which are not allowed |
| usually a verb or verb phrase | print, calculate, convert | a method does something |
not main, not a keyword | — | main is reserved as the entry point |
The naming set is now complete: UpperCamelCase for classes, lowerCamelCase for methods and variables, ALL_CAPS for constants. None of it is enforced by the compiler and all of it is relied on by readers.
Trap
The mistake. Putting the new method inside main, where the statements go.
public static void main(String[] args) {
public static void newLine() { // illegal here
System.out.println();
}
newLine();
}| what is inside what | legal? |
|---|---|
| a method inside a class | yes — this is how it works |
| a statement inside a method | yes |
| a method inside a method | no — illegal start of expression |
The nesting from Lesson 3a decides this: a class contains methods, and a method contains statements. A method is not a statement, so it cannot go where statements go.
Methods are siblings inside the class, not nested in one another.
public class NewLine {
public static void newLine() {
System.out.println();
}
public static void main(String[] args) {
newLine();
}
}| level | contains |
|---|---|
| class NewLine | newLine and main — side by side |
| method newLine | one statement |
| method main | one statement, which invokes newLine |
The order the methods appear in the file does not matter at all — which is the subject of the next section, and surprises most people the first time.
Definition probe
The nesting from Lesson 3a, applied to a real file.
Sort into buckets
Sort each line of the NewLine program.
Fill the middle
A void method with no parameters.
Fill in the blanks
public static void separator() separator()
public static void main(String[] args) ___};
}
Why: void says the method returns no result, which is right for something that only prints. Invoking it is separator() — with the parentheses, because without them you have named the method rather than calling it, and on a line of its own, because there is no value to catch.
Explain it to yourself
It is one line long and its body is shorter than its name.
Discussion prompt
newLine contains a single statement, System.out.println();. Writing the method is more typing than just calling println. Give the strongest argument you can for defining it anyway.
Hint: Think about what the two versions say to a reader.
Answer:
The strongest argument is naming. System.out.println(); with no argument looks like a mistake — a reader wonders whether something was left out. newLine(); says exactly what is intended.
The second argument is that it gives you something to build on. threeLine is written in terms of newLine, so the concept 'a blank line' exists in one place. This is the smallest possible example of the technique the whole chapter is about: name a block of statements so you can compose with it.
Section
Section 4.2
Concept
When you look at a class containing several methods, it is tempting to read it top to bottom. But that is not the flow of execution — the order the program actually runs. The NewLine program runs its methods in the opposite order to the one they are listed in.
Figure (svg): A pipeline showing execution entering main, detouring into threeLine, then into newLine, and returning each time
One method can invoke another, so the detours nest. Java keeps track: when println finishes it returns to newLine, when newLine finishes it returns to threeLine, and when threeLine finishes it returns to main.
Picture it
The NewLine program lists newLine first and main last, and runs them in the reverse order. Reading a file top to bottom tells you what exists, not what happens.
Figure (svg): Two panels comparing the order methods appear in the file against the order they execute
This is why a program's structure has to be traced rather than read. The file is a list of definitions; the execution is a path through them.
Worked example
Follow the flow of execution through NewLine, statement by statement, noting each jump and each return.
public static void main(String[] args) {
System.out.println("First line.");
threeLine();
System.out.println("Second line.");
}| step | where we are | what happens |
|---|---|---|
| 1 | main | println("First line.") — output appears |
| 2 | main | threeLine() — jump away |
| 3 | threeLine | newLine() — jump again |
| 4 | newLine | println() — a blank line, then return |
| 5 | threeLine | newLine() twice more, each returning |
| 6 | main | back where we left off |
| 7 | main | println("Second line.") |
Start at the first statement of main.
Why: Always, regardless of where main appears in the file.
At an invocation, jump to the method's first line.
Why: Everything after the call in the current method waits.
Run all the statements there.
Why: Including any further invocations, which nest the same way.
Return to exactly where you left off.
Why: Not to the start of the calling method — to the statement after the call.
Verify: Count the outputs: one line, three blanks, one line — five in total, in that order.
Why: If your trace produced the two text lines adjacent, you returned to the wrong place after threeLine. The return goes to the statement AFTER the call, which is what makes the gap appear between them.
Ranking
For the NewLine program, from the moment the program starts.
Put in order
Why: Execution always begins at main's first statement, and each invocation is a detour that returns to the statement immediately after the call. The step people misplace is the return: control comes back to where it left off in main, not to the beginning of main.
Concept
Beginners often wonder why it is worth writing other methods when everything could go in main. The NewLine example demonstrates several reasons.
| reason | in NewLine | in general |
|---|---|---|
| naming a block | newLine() says what println() with no argument means | code that explains itself needs fewer comments |
| removing repetition | threeLine calls newLine three times | nine newlines would be threeLine three times |
| breaking down a problem | one method per subproblem | you can focus on each part in isolation |
| testing separately | newLine can be checked on its own | a complex program is easier to get working when each part is known good |
Think Java calls the last one perhaps the most important: organising code into multiple methods lets you test individual parts of your program separately. That idea drives the whole of Lesson 4b's incremental development.
Trap
The misreading. Methods run in the order they appear, so newLine runs first.
public class NewLine {
public static void newLine() { ... } // 1st in the file
public static void threeLine() { ... } // 2nd
public static void main(String[] args) { // 3rd
System.out.println("First line.");
threeLine();
}
}| prediction from file order | what actually happens |
|---|---|
| newLine runs first — a blank line | nothing runs until main starts |
| then threeLine | main's first statement runs first |
| then main | the others run only when invoked |
A method definition does not do anything. It says what would happen if the method were invoked. Nothing in the file runs until main is entered.
Execution begins at main, and methods run only when invoked.
// The file can list them in any order at all:
public class NewLine {
public static void main(String[] args) { ... } // first in the file
public static void threeLine() { ... }
public static void newLine() { ... }
}
// The program behaves identically.| what decides | the order of execution |
|---|---|
| where main is in the file | no effect |
| where the other methods are | no effect |
| which statements invoke which methods | this is what decides it |
So the order of methods in a file is purely a readability decision. Many programmers put main first so a reader meets the top-level story before the details; Think Java's examples usually put it last.
Prediction
The three method definitions are reordered in the file; nothing else changes.
Predict first
If you move main to the top of the file, above newLine and threeLine, what happens?
Correct: Nothing — the output is identical
Why: Programs always begin at the first statement of main regardless of where main is in the source file, and every other method runs only when it is invoked. The order of definitions in a file is a readability decision with no effect whatsoever on behaviour.
Invariant
Step through the detours and watch which method is running at each moment.
Step through it
At the third frame, how many methods have started but not yet finished?
Three — main, threeLine and newLine. Each is waiting for the one it called. That stack of waiting methods is exactly what the next section draws.
Socratic
The detours nest three deep and come back in the right order every time.
Discussion prompt
When println finishes, Java returns to newLine; when newLine finishes, to threeLine; then to main. What information must Java be storing to get this right, and where do you think it keeps it?
Hint: It has to remember more than one place at once.
Answer:
It must remember, for every method that has started but not finished, which statement to come back to — and it must remember them in order, because the most recently started method is the first to finish.
That is a stack: last in, first out. Java keeps it in a region of memory called the call stack, and the next section draws it. This is also why a runaway recursion in Chapter 8 produces a StackOverflowError — the store of waiting methods is finite.
Section
Section 4.3
Concept
When you invoke a method, you provide the arguments. When you define a method, you name the parameters — variables that indicate what arguments are required.
public class PrintTwice {
public static void printTwice(String s) {
System.out.println(s);
System.out.println(s);
}
public static void main(String[] args) {
printTwice("Don't make me say this twice!");
}
}| term | where it appears | in this example |
|---|---|---|
| parameter | in the method definition | String s |
| argument | at the call site | "Don't make me say this twice!" |
| parameter passing | just before the method runs | the argument is assigned to s |
argument — A value you provide when you call a method. It must have the type the method expects.
parameter — A variable named in a method definition, holding a value the method requires before it can run.
parameter passing — The process of assigning an argument value to a parameter variable.
Before the method executes, the argument gets assigned to the parameter. The process is called parameter passing, because the value is passed from outside the method to the inside.
Picture it
Parameter passing is just an assignment — the same operation from Chapter 2, happening automatically at the moment of the call.
Figure (svg): A trace strip showing an argument value being assigned to a parameter variable just before the method body runs
An argument can be any kind of expression, not just a literal. If you have a String variable you can use its value as an argument — and Java evaluates the expression first, then assigns the result.
Worked example
The value you provide as an argument must have the same, or a compatible, type as the parameter. Getting it wrong produces a long message that is more helpful than it first looks.
printTwice(17); // syntax error| line of the message | what it tells you |
|---|---|
| method printTwice cannot be applied to given types | the call does not match any definition |
| required: java.lang.String | what the parameter is |
| found: int | what you passed |
| actual argument int cannot be converted to String | and Java will not convert it for you |
Read required and found first.
Why: Those two lines are the whole diagnosis: a String was needed, an int was supplied.
Note that Java will not convert here.
Why: It will not turn the integer 17 into the string "17" automatically, even though + would.
Contrast with a case where it does convert.
Why: Math.sqrt requires a double, but Math.sqrt(25) works — the int 25 is converted to 25.0, because nothing is lost.
Fix by supplying the right type.
Why: printTwice("17"); passes a String.
Verify: Change the argument to "17" and confirm it compiles and prints the text twice.
Why: The rule to carry away is the same one from Lesson 2b: widening conversions happen automatically, others do not. int to double is silent; int to String is not a conversion at all.
Definition probe
The distinction is about which side of the call you are on.
Sort into buckets
Sort each item from the printTwice example.
String s in the method header; int hour in printTime(int hour, int minute)"Don't make me say this twice!" in main; message passed in printTwice(message)Concept
Parameters and other variables exist only inside the methods where they are defined. That is why they are called local variables.
public static void printTwice(String s) {
System.out.println(s); // s exists here
}
public static void main(String[] args) {
String message = "Never say never.";
printTwice(message); // message exists here
// System.out.println(s); // error: cannot find symbol
}| variable | exists in | visible in main? | visible in printTwice? |
|---|---|---|---|
| s | printTwice | no | yes |
| message | main | yes | no |
| args | main | yes | no |
local variable — A variable declared inside a method. It cannot be accessed from outside that method.
In the printTwice example there is no such thing as s in main, and no such thing as message inside printTwice. Trying to use either in the wrong place is a compile error — cannot find symbol.
Trap
A very common beginner mistake, and the error message does not obviously explain it.
int hour = 11;
int minute = 59;
printTime(int hour, int minute); // syntax error| what the compiler sees | what it expected |
|---|---|
int hour — a variable declaration | an expression representing a value |
int minute — another declaration | another expression |
| a declaration inside a call's parentheses | illegal |
The compiler reads int hour as a declaration rather than as a value. You would never write printTime(int 11, int 59); — and this is the same mistake with variables in place of the literals.
Types go in the definition; values go at the call.
// definition — types named here:
public static void printTime(int hour, int minute) { ... }
// call — values only:
int hour = 11;
int minute = 59;
printTime(hour, minute);| where | what you write |
|---|---|
| method definition | a type and a name for each parameter |
| method call | an expression for each argument — and nothing else |
One way to remember it: the definition is a promise about what is required; the call is the delivery. You state the type once, where the promise is made.
Prediction
The parameter is a String.
public static void printTwice(String s) { ... }
// which of these is legal?
printTwice("17");
printTwice(17);| call | argument type | parameter type | legal? |
|---|---|---|---|
| printTwice("17") | String | String | yes |
| printTwice(17) | int | String | no |
Predict first
Which of the two calls compiles?
Correct: Only printTwice("17")
Why: The argument must have the same or a compatible type as the parameter, and Java will not convert the integer 17 to the string "17" automatically. Note that it WOULD convert an int to a double — Math.sqrt(25) works — because that conversion loses nothing. int to String is not a widening conversion; it is a different kind of change entirely.
Error analysis
A student added print statements to check their values, and two of the four lines will not compile.
Annotate
s is printTwice's parameter, so it exists throughout printTwice's body.message is a local variable of main. Inside printTwice there is no such thing, and the compiler says cannot find symbol.message is declared in main and used in main.s belongs to printTwice. In main there is no such thing.This isolation is a feature rather than a restriction. It means you can read a method and know that nothing outside it can interfere with its variables.
Fill the middle
A method that greets a named person.
Fill in the blanks
public static void greet(String name) "Ada"
public static void main(String[] args) ___});
}
Why: The parameter needs a type and a name in the definition — String name — and the call supplies a value of that type with no type written. Writing greet(String "Ada") would be a syntax error, because the compiler would read it as a declaration rather than a value.
Section
Section 4.4
Concept
A method can take any number of parameters. To invoke it you provide the same number of arguments, of compatible types, in the same order.
public class PrintTime {
public static void printTime(int hour, int minute) {
System.out.print(hour);
System.out.print(":");
System.out.println(minute);
}
public static void main(String[] args) {
int hour = 11;
int minute = 59;
printTime(hour, minute);
}
}| parameter | receives | value |
|---|---|---|
| hour (in printTime) | the first argument | 11 |
| minute (in printTime) | the second argument | 59 |
| output | — | 11:59 |
printTime has two parameters named hour and minute. And main has two variables also named hour and minute. Although they have the same names, these are not the same variables — and that is the point of this section.
Picture it
The hour in printTime and the hour in main refer to different memory locations, and they can hold different values.
Figure (svg): A stack diagram with main on top holding hour 11 and minute 59, and printTime below holding hour 12 and minute 0
This is the picture after printTime(hour + 1, 0). Java evaluated the arguments first — giving 12 and 0 — and assigned those to the parameters. Inside printTime, hour is 12; in main it is still 11.
Worked example
An argument can be any expression. Java works out its value first, and only then assigns it to the parameter.
int hour = 11;
int minute = 59;
printTime(hour + 1, 0);| step | in main | in printTime |
|---|---|---|
| before the call | hour = 11, minute = 59 | does not exist yet |
| evaluate the arguments | hour + 1 is 12; 0 is 0 | — |
| assign to the parameters | hour = 11, minute = 59 | hour = 12, minute = 0 |
| the body runs | unchanged | prints 12:0 |
| after the return | hour = 11, minute = 59 | gone |
Evaluate each argument expression.
Why: hour + 1 uses main's hour, which is 11, giving 12.
Assign the results to the parameters, in order.
Why: The first argument goes to the first parameter.
Note that main's variables are untouched.
Why: Adding one to hour in the argument did not change main's hour.
And that changes inside the method do not escape.
Why: If printTime modified one of its parameters, that change would have no effect on the variables in main.
Verify: Print main's hour after the call and expect 11, not 12.
Why: That is the whole content of this section: the two hour variables share a name and nothing else. The name is a coincidence of your choosing, and Java attaches no meaning to it.
Prediction
The method modifies its parameter.
public static void bump(int x) {
x = x + 10;
}
public static void main(String[] args) {
int x = 1;
bump(x);
System.out.println(x);
}| step | main's x | bump's x |
|---|---|---|
| int x = 1; | 1 | — |
| bump(x) — parameter passing | 1 | 1 |
| x = x + 10 inside bump | 1 | 11 |
| after the return | 1 | gone |
Predict first
What is displayed?
Correct: 1
Why: Parameter passing assigns a copy of the argument's value to the parameter, so bump's x is a different variable that happens to share a name. Changing it has no effect on main's x. Sharing the name is perfectly legal — the two variables are in different methods and cannot see each other.
Concept
It might seem safer if Java forbade two methods from using the same variable name. It does not, and the reason is worth understanding.
| if names had to be unique across methods | as it actually is |
|---|---|
| you would have to know every other method's names | you only have to know your own |
| adding a method could break an existing one | methods cannot interfere with each other |
| a good name could only be used once in a program | hour means hour wherever it is natural |
| large programs would become impossible | methods stay independent however many there are |
Locality is what makes methods composable. Because a method's variables are invisible outside it, you can read one method and be sure that nothing elsewhere is quietly changing its values. That guarantee is why the same name in two methods is not merely tolerated but normal.
Trap
The assumption. The parameter and the argument variable are somehow linked.
public static void addOne(int n) {
n = n + 1;
}
public static void main(String[] args) {
int count = 5;
addOne(count);
System.out.println(count); // predicted 6
}| step | count (in main) | n (in addOne) |
|---|---|---|
| before the call | 5 | — |
| parameter passing | 5 | 5 — a copy |
| n = n + 1 | 5 | 6 |
| after the return | 5 | gone |
The output is 5. This is Chapter 2's b = a lesson again in a new setting: assignment copies a value. Parameter passing is an assignment, so the parameter gets a copy.
To get a value out of a method, return it — which is Lesson 4b.
public static int addOne(int n) {
return n + 1;
}
public static void main(String[] args) {
int count = 5;
count = addOne(count);
System.out.println(count); // 6
}| step | count | what changed it |
|---|---|---|
| int count = 5; | 5 | the declaration |
| count = addOne(count); | 6 | the assignment in MAIN, using the returned value |
Notice where the change happens: in main, on the line with the =. The method computed a value; only the caller decided to store it. That separation is what makes methods safe to call.
Discrimination
public static void printTime(int hour, int minute)
Sort into buckets
Sort each call by whether it compiles.
Pattern
Step through printTime(hour + 1, 0) and watch both methods' variables.
Step through it
At the third frame, how many variables called hour exist?
Two — one in each frame, holding 11 and 12. They share a name and nothing else, which is exactly what the stack diagram is drawn to make visible.
Counterexample
You have just been told it cannot. Ask whether that is always what you want.
Discussion prompt
A method cannot change its caller's variables. Think of a task where that restriction is inconvenient, and suggest how you would work around it with what you know so far.
Hint: What if the method needs to produce two values?
Answer:
The obvious case is a method that should produce more than one result — splitting inches into feet and remaining inches, say. Returning one value handles one of them.
With what you know now, the workaround is to write two methods, or to return one value and recompute the other. Neither is elegant.
The real answer comes in Chapter 10: objects can be modified by the methods they are passed to, because what gets copied is a reference rather than the value itself. That is precisely why Chapters 9 and 10 spend so long on the difference between primitives and objects — the rule you have just learned holds for primitives and is exactly what changes for objects.
Section
Section 4.5
Concept
One way to keep track of variables is to draw a stack diagram — a memory diagram that shows the currently running methods. For each method there is a box, called a frame, containing that method's parameters and local variables. The method's name appears outside the frame; the variables inside.
Figure (svg): A stack diagram with main at the top holding hour and minute, and printTime below with its own hour and minute
stack diagram — A graphical representation of the variables belonging to each method, with the calls stacked in the flow of execution.
frame — In a stack diagram, the box holding one method's parameters and local variables, with their current values.
scope — The area of a program where a variable can be used.
Think Java draws main on top, because it executed first, with each called method below it. Every stack diagram in this course follows that convention.
Picture it
The NewLine program at the moment newLine is running: three methods have started and none has finished.
Figure (svg): A stack diagram with main at the top, threeLine below it, and newLine at the bottom, each with its own frame
None of these frames has any interesting variables, which makes the shape easy to see: the depth of the stack is how many methods are waiting. When newLine returns its frame disappears, then threeLine's, and main is alone again.
Worked example
Like memory diagrams, stack diagrams show a particular point in time. Pick the moment first, then draw only what exists then.
public static void printTime(int hour, int minute) {
System.out.print(hour); // <- draw the diagram here
System.out.print(":");
System.out.println(minute);
}
public static void main(String[] args) {
int hour = 11;
int minute = 59;
printTime(hour + 1, 0);
}| frame | variable | value |
|---|---|---|
| main | args | the command-line arguments |
| main | hour | 11 |
| main | minute | 59 |
| printTime | hour | 12 |
| printTime | minute | 0 |
Choose the moment and write it down.
Why: Here: the first statement of printTime, just before anything is printed.
Draw a frame for every method that has started and not finished.
Why: main and printTime — two frames, main on top.
Put each method's parameters and local variables inside its own frame.
Why: main has args, hour and minute; printTime has its own hour and minute.
Fill in the current values.
Why: printTime's hour is 12, because the argument hour + 1 was evaluated before the call.
Verify: Check that no variable appears in two frames unless it genuinely exists in both — here hour does, with different values.
Why: And check the depth against the flow of execution: two methods have started and neither has returned, so there are exactly two frames.
Prediction
The NewLine program, at the moment println is executing inside newLine.
Predict first
How many frames are on the stack (counting only methods you wrote)?
Correct: 3 — main, threeLine, newLine
Why: main called threeLine, which called newLine, and none of them has finished — so all three have frames. println would add a fourth, but it belongs to the Java library rather than to your program. The depth of the stack is exactly the number of methods that have started and not yet returned.
Concept
Stack diagrams help you visualise the scope of a variable — the area of a program where it can be used — and they are a good mental model for how variables and methods work at run time.
| question | how the diagram answers it |
|---|---|
| Can this method see that variable? | only if it is in this method's own frame |
Why is there a cannot find symbol error? | the name is in a different frame |
Which hour does this line mean? | the one in the frame you are currently inside |
| How deep is the program? | the number of frames |
| What happens when this method returns? | its frame disappears, taking its variables with it |
Learning to trace execution on paper or a whiteboard is a useful skill for communicating with other programmers. Tools can draw these for you too — Java Tutor lets you step through a program forwards and backwards and watch the frames appear and disappear.
Trap
The mistake. Drawing every method that has run, rather than every method still running.
public static void main(String[] args) {
newLine();
threeLine(); // <- draw the diagram here, inside threeLine
}| method | has it run? | is it still running? | does it get a frame? |
|---|---|---|---|
| main | yes | yes — waiting | yes |
| newLine (first call) | yes | no — it returned | no |
| threeLine | yes | yes | yes |
The first newLine() finished before threeLine() started. Its frame — and its variables — are gone. Drawing it suggests its variables are still accessible, which is exactly the wrong idea.
A frame exists only while its method is running.
// at the marked moment, exactly two methods have started
// and not finished: main, and threeLine.| frame | why it is there |
|---|---|
| main | started, and waiting for threeLine to return |
| threeLine | currently running |
When a method returns, its frame is removed and its local variables cease to exist. That is why you cannot 'look at' a variable after its method has finished — there is nothing left to look at, which is also why a method must return a value if the caller is to keep it.
Definition probe
Using the printTime stack diagram: main has hour, minute and args; printTime has hour and minute.
Sort into buckets
Sort each usage by whether it compiles.
hour inside printTime; reading minute inside mainargs inside printTime; reading printTime's hour from mainInvariant
Step through the NewLine program and watch the stack grow and shrink.
Step through it
What is the maximum number of frames this program ever has?
Three. The stack grows on every call and shrinks on every return, and its depth at any moment is the number of methods waiting. Chapter 8's recursion is what happens when that depth becomes large.
Explain it
Draw before you speak.
Discussion prompt
A classmate has written System.out.println(s); in main, where s is printTwice's parameter, and cannot understand the cannot find symbol error. Draw the stack diagram and use it to explain. Then answer their follow-up: 'so how DO I get the value out of the method?'
Hint: The second question is what the next lesson is about.
Answer:
Draw two frames: main on top with its own variables, printTwice below with s inside it. s is in printTwice's frame, so it exists only while printTwice is running and only inside that method. From main there is no s to find — which is precisely what the compiler said.
And to get a value out: the method has to return it, and main has to catch it in a variable. That is the only way anything crosses the boundary in that direction — an argument goes in, a return value comes out.
If your explanation reached for the diagram first and the words second, you have the right habit. Scope is a spatial idea, and the picture does most of the work.
Comparison
Three things that are easy to blur. Fill the blanks.
Comparison matrix
| argument | parameter | other local variable | |
|---|---|---|---|
| written where | at the call | in the method definition's parentheses | inside the method body |
| has a type written? | no — just a value | yes | yes, in its declaration |
| who gives it a value | you, when calling | parameter passing, automatically | an assignment you write |
| how long it exists | only until the call is made | while the method runs | while the method runs |
The bottom row is what a stack diagram draws: everything in a frame lives exactly as long as that frame, and vanishes when the method returns.
Pattern
A method call is three separate events, and keeping them apart resolves nearly every confusion in this lesson.
| event | what happens | where |
|---|---|---|
| 1. evaluate the arguments | each expression becomes a value | in the calling method |
| 2. pass the parameters | each value is ASSIGNED to a parameter | a new frame is created |
| 3. run and return | the body executes, then the frame is discarded | in the called method |
Check
Work it out before you click.
public class Order {
public static void b() { System.out.println("B"); }
public static void a() { System.out.println("A"); b(); }
public static void main(String[] args) {
System.out.println("start");
a();
System.out.println("end");
}
}| step | output |
|---|---|
| main's first println | start |
| a() is invoked; its println runs | A |
| b() is invoked from a | B |
| control returns to main | end |
Check your understanding
What does this program display?
Answer: A
Why: Execution begins at main regardless of where the methods appear in the file. main prints 'start', then calls a, which prints 'A' and calls b, which prints 'B'; control then returns to main, which prints 'end'.
Check
Work it out before you click.
public static void triple(int n) {
n = n * 3;
System.out.println(n);
}
public static void main(String[] args) {
int n = 4;
triple(n);
System.out.println(n);
}| step | main's n | triple's n |
|---|---|---|
| int n = 4; | 4 | — |
| triple(n) | 4 | 4 |
| n = n * 3 inside triple | 4 | 12 |
| after the return | 4 | gone |
Check your understanding
What does this program display?
Answer: A
Why: triple prints its own n after tripling it, giving 12; main then prints its own n, which was never changed, giving 4. Parameter passing assigns a copy of the value, so the two variables share a name and nothing else.
Check
Work it out before you click.
public static void show(String label) {
int count = 3;
System.out.println(label + count);
}
public static void main(String[] args) {
show("n = ");
System.out.println(count);
}| variable | declared in | used in | legal? |
|---|---|---|---|
| label | show's parameters | show | yes |
| count | show's body | show | yes |
| count | show's body | main | no |
Check your understanding
Why does this fail to compile?
Answer: A
Why: count is declared inside show, so it exists only in show's frame and only while show is running. By the time main's println runs, show has returned and its frame — including count — is gone, so the compiler reports 'cannot find symbol'.
Real world
The stack diagram is not a teaching device invented for this book. It is a picture of something real in the machine.
Discussion prompt
You have seen a stack trace in an exception message twice now — in Lesson 2b and Lesson 3b. Look back at the shape of one. What is it a list of, and how does that connect to what you drew today?
Hint: Count the lines in a stack trace and count the frames in a diagram.
Answer:
A stack trace is the stack diagram, printed. Each at ... line is one frame — a method that had started and not finished when the exception was thrown, listed from the deepest outward.
That is why the last line is your main and the first is wherever it actually broke: you are reading the frames from the top of the stack down to the bottom. The advice from Lesson 3b — read the first line for what, the last for where — is advice about reading a stack diagram.
Every language with methods has one, under one name or another, and every debugger shows it. Learning to draw it by hand is what makes the debugger's version readable.
Commit first
Commit to an answer and to your confidence.
Predict first
A method's parameter is named hour, and its caller also has a variable named hour. How many variables named hour exist while the method is running?
Correct: Two — one in each frame
Why: Each method gets its own frame, and each frame holds its own parameters and local variables. Two variables sharing a name in different methods are entirely separate memory locations that can hold different values — in the book's example, main's hour is 11 while printTime's is 12. The shared name is a coincidence of your choosing, and Java attaches no meaning to it whatsoever.
Explain it
Two minutes, drawing as you go.
Discussion prompt
Explain to someone who has only written single-method programs what happens, step by step, when one method calls another. Use the words frame, argument, parameter and return, and draw the stack at two different moments.
Hint: The two moments worth drawing are just after the call and just after the return.
Answer:
When main reaches a call, it first works out the value of each argument. Then a new frame appears for the called method, holding its parameters — each one assigned a copy of the matching argument. main pauses where it is. The new method runs using only what is in its own frame. When it returns, its frame is thrown away and main carries on at the statement right after the call.
The two drawings should differ in exactly one way: the second has one fewer frame. If your listener asks 'but where did the method's variables go?', the answer — they no longer exist — is the whole reason the next lesson is about return values.
Exit ticket
One question before you close the deck.
Predict first
Where does execution go when a method finishes?
Correct: Back to the statement immediately after the call that invoked it
Why: A method invocation is a detour: you jump to the invoked method, run its statements, and then come back and pick up exactly where you left off. Not at the start of the calling method, and not at the next definition in the file — at the very next statement after the call. That is what makes the blank lines in NewLine appear between the two lines of text rather than before or after both.
Connect it up
One page, from memory.
Draw it
Draw the stack diagram for the NewLine program at three moments: just after main starts, while newLine is running, and just after threeLine returns. Label each frame with its method name and put any variables inside. Then, beside the diagrams, write the three events of a method call in order — evaluate the arguments, pass the parameters, run and return — and one sentence saying why a method cannot change its caller's variables.
Recap
Five sections that turn a program from a single list of statements into a set of named, independent, testable pieces.
| if you remember one thing | it is this |
|---|---|
| about execution | the file lists definitions; main decides the order |
| about parameters | passing is copying |
| about scope | a variable lives in one frame and dies with it |
void declares a method that returns no result, and public static will precede every method you write for now.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.