Method Design
How do we write small, clear methods that are easy to understand and reuse?
By the end of this lecture you will be able to
- Write methods that share a name, and work out which one Java picks in a call
- Explain why a method does not know the variables of
main - Prove with the debugger that primitives are passed by value
- Break a long program into small methods, each with a clear name and one responsibility
- Trace a method that calls itself and identify its base case
- Reuse
isPrime,gcdandpowerin new programs
In this lecture
- Several methods can share a name if their parameters differ
- Java picks the method from the number and types of the arguments
- Each version must clearly differ in its parameters
- A variable lives only inside the method that declares it
- A method receives a copy of the value, not the variable itself
- The swap method fails, and the debugger shows why
- A program written entirely in
mainis hard to follow - Decomposition changes the shape of a program, not its behaviour
- A good name describes one job
- A method can call itself with a smaller problem
- The base case stops the chain, and the recursive step moves toward it
- Without a base case the stack fills up and the program stops
isPrimeis a tool you write once and keep usinggcdandpowerbuild solutions to bigger problems- Review questions
Several methods can share a name if their parameters differ
You already use this idea without noticing: println accepts text, a number or a character, because Java has several methods called println. Defining more than one method with the same name and different parameters is called overloading.
Three methods, one name
Predict which method runs on each line. Then press "Debug line by line" and "Step into" to check. Add a fourth version that takes four whole numbers, and call it.
public class OverloadedAdd {
static int add(int a, int b) {
return a + b;
}
static double add(double a, double b) {
return a + b;
}
static int add(int a, int b, int c) {
return a + b + c;
}
public static void main(String[] args) {
System.out.println(add(3, 4));
System.out.println(add(3.5, 4.0));
System.out.println(add(1, 2, 3));
}
}7 7.5 6
Java picks the method from the number and types of the arguments
- Rule out any with the wrong number of parameters
- Prefer an exact match of the types
- Otherwise accept a conversion to a wider typeSuch as
chartointandinttodouble. It never converts to a narrower type.
Which method does Java pick for each call? Watch the last line.
public class PickOverload {
static void show(int n) {
System.out.println("int: " + n);
}
static void show(double n) {
System.out.println("double: " + n);
}
static void show(String n) {
System.out.println("String: " + n);
}
public static void main(String[] args) {
show(5);
show(5.0);
show("5");
show('A');
}
}Show the output
int: 5 double: 5.0 String: 5 int: 65
Why? There is no method taking a char, and a char does not become a String automatically. The closest wider type is int, so the letter A becomes its code, 65.
One method takes a double, and we give it an int. What does it print?
public class WidenInt {
static double half(double x) {
return x / 2;
}
public static void main(String[] args) {
System.out.println(half(5));
}
}Show the output
2.5
Why? The argument 5 becomes 5.0 before entering the method, so the division is decimal and the fraction is kept.
The only method is static void show(int n). What happens with show(2.5);?
show((int) 2.5).Each version must clearly differ in its parameters
The return type alone is not enough
The two methods differ only in return type. Change the parameter of the second version to double and call it with 1.5.
public class TwiceTwo {
static int twice(int n) {
return n * 2;
}
static double twice(int n) {
return n * 2.0;
}
public static void main(String[] args) {
System.out.println(twice(4));
}
}is already defined in classShow the fix
public class TwiceTwo {
static int twice(int n) {
return n * 2;
}
static double twice(double n) {
return n * 2.0;
}
public static void main(String[] args) {
System.out.println(twice(4));
System.out.println(twice(1.5));
}
}8 3.0
Why? When reading twice(4) Java looks only at the arguments, so it cannot tell the methods apart by return type. That is why it treats the second as a repeat of the first.
An ambiguous call
Each method needs one conversion to accept total(1, 2), so Java cannot tell which to pick. Make the second argument 2.0.
public class Ambiguous {
static double total(int a, double b) {
return a + b;
}
static double total(double a, int b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(total(1, 2));
}
}reference to total is ambiguousShow the fix
public class Ambiguous {
static double total(int a, double b) {
return a + b;
}
static double total(double a, int b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(total(1, 2.0));
}
}3.0
Why? Neither method is a clear winner, so Java refuses to guess. After the change, the first method matches both arguments exactly.
A variable lives only inside the method that declares it
The region where a variable's name is known is its scope. The variables of main are not shared across the class; they are local to main. The right way to give a value to another method is an argument.
The method cannot see the variables of main
Give showScore a parameter int score, and pass the value in the call.
public class ShowScore {
static void showScore() {
System.out.println(score);
}
public static void main(String[] args) {
int score = 90;
showScore();
}
}cannot find symbolShow the fix
public class ShowScore {
static void showScore(int score) {
System.out.println(score);
}
public static void main(String[] args) {
int score = 90;
showScore(score);
}
}90
Why? The message looks like a spelling mistake, even though the name is spelled correctly. The problem is where it is used: no variable has this name inside showScore.
Two variables with one name in two different methods. What does it print?
public class TwoBoxes {
static void change() {
int count = 100;
System.out.println(count);
}
public static void main(String[] args) {
int count = 1;
change();
System.out.println(count);
}
}Show the output
100 1
Why? The matching names are a coincidence: each method has its own box. In the debugger you see one count in the change() frame and another in the main() frame.
A method receives a copy of the value, not the variable itself
In a call, the value of the argument is copied into the parameter's box, and after that the two boxes have no connection. This is called pass by value.
The method changes its parameter. Does x change in main?
public class AddTen {
static void addTen(int n) {
n = n + 10;
System.out.println(n);
}
public static void main(String[] args) {
int x = 5;
addTen(x);
System.out.println(x);
}
}Show the output
15 5
Why? n is a copy that started at 5 and became 15, while nothing touched x. The box n disappears when the method ends.
Returning delivers the result
To get the result to main, the method returns it and the caller assigns it to the variable. Trace the program line by line, then delete x = from the call line and predict the output before running.
public class AddTenReturn {
static int addTen(int n) {
n = n + 10;
return n;
}
public static void main(String[] args) {
int x = 5;
x = addTen(x);
System.out.println(x);
}
}15
The swap method fails, and the debugger shows why
Swapping with temp worked in the variables lecture. Let us move it into a method and expect it to work the same way.
A swap that never reaches main
Predict the two lines, then run. After that, follow the steps below.
public class FailedSwap {
static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("in swap: a = " + a + ", b = " + b);
}
public static void main(String[] args) {
int a = 5;
int b = 9;
swap(a, b);
System.out.println("in main: a = " + a + ", b = " + b);
}
}in swap: a = 9, b = 5 in main: a = 5, b = 9
- Press "Debug line by line", then "Next line" until
swap(a, b);Themain()frame has boxesa = 5andb = 9. - Press "Step into"The frame
swap()appears abovemain(), with two more boxes that have the same names and values. - Press "Next line" until the
}ofswapThe values swap in theswap()frame, while themain()frame stays as it was:a = 5andb = 9. - Press "Next line" once moreThe frame
swap()disappears, along with all its work. The program swapped only two copies.
Why did a and b stay unchanged in main?
x and y would change nothing, and neither would removing void. The reason is that an argument's value is copied to a new box in a new frame.A program written entirely in main is hard to follow
A menu in one method
Run the program with the ready input: add 3 and 4, check 7, try a choice that does not exist, then exit. Then answer: where does the adding part start and end? How many times do we write a request for a number?
import java.util.Scanner;
public class MenuBefore {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int choice;
do {
System.out.println("1) Add");
System.out.println("2) Even or odd");
System.out.println("0) Exit");
System.out.print("Choice: ");
choice = input.nextInt();
if (choice == 1) {
System.out.print("a: ");
int a = input.nextInt();
System.out.print("b: ");
int b = input.nextInt();
System.out.println("Sum = " + (a + b));
} else if (choice == 2) {
System.out.print("n: ");
int n = input.nextInt();
if (n % 2 == 0) {
System.out.println("even");
} else {
System.out.println("odd");
}
} else if (choice != 0) {
System.out.println("Unknown choice");
}
} while (choice != 0);
System.out.println("Bye");
}
}1) Add 2) Even or odd 0) Exit Choice: a: b: Sum = 7 1) Add 2) Even or odd 0) Exit Choice: n: odd 1) Add 2) Even or odd 0) Exit Choice: Unknown choice 1) Add 2) Even or odd 0) Exit Choice: Bye
- Write what the program does in short sentences: show the menu, add, check.
- Turn each sentence into a method with that name.
- Find the repeated lines and turn them into a small method.
- Leave only the big decisions of the program in
main.
Decomposition changes the shape of a program, not its behaviour
The same menu in small methods
The same input and exactly the same output. Read main first: it now explains the program. A Scanner can be passed as an argument like any other value. Change the text Exit in printMenu and run: one edit shows up every time. Then plan on paper a third choice that multiplies two numbers: how many methods do you add, and how many lines change in main?
import java.util.Scanner;
public class MenuAfter {
static void printMenu() {
System.out.println("1) Add");
System.out.println("2) Even or odd");
System.out.println("0) Exit");
}
static int readInt(Scanner input, String prompt) {
System.out.print(prompt);
return input.nextInt();
}
static boolean isEven(int n) {
return n % 2 == 0;
}
static void handleAdd(Scanner input) {
int a = readInt(input, "a: ");
int b = readInt(input, "b: ");
System.out.println("Sum = " + (a + b));
}
static void handleEvenOdd(Scanner input) {
int n = readInt(input, "n: ");
if (isEven(n)) {
System.out.println("even");
} else {
System.out.println("odd");
}
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int choice;
do {
printMenu();
choice = readInt(input, "Choice: ");
if (choice == 1) {
handleAdd(input);
} else if (choice == 2) {
handleEvenOdd(input);
} else if (choice != 0) {
System.out.println("Unknown choice");
}
} while (choice != 0);
System.out.println("Bye");
}
}1) Add 2) Even or odd 0) Exit Choice: a: b: Sum = 7 1) Add 2) Even or odd 0) Exit Choice: n: odd 1) Add 2) Even or odd 0) Exit Choice: Unknown choice 1) Add 2) Even or odd 0) Exit Choice: Bye
A good name describes one job
| Weak name | Better name | Why |
|---|---|---|
doStuff | calculateAverage | A method does something, so start its name with a verb that says what |
check | isPrime | A boolean method reads as a question |
readAndPrintTotal | readInt, printTotal | The word And reveals two jobs, so separate them |
Which design for working out a bill total is easiest to reuse?
Decompose a receipt program
Read price and quantity, both int. Write three methods: subtotal returns the product, discount returns 10 if the subtotal is 100 or more and 0 otherwise, and printReceipt prints the three lines as in this example: Subtotal: 120, then Discount: 10, then Total: 110.
import java.util.Scanner;
public class Receipt {
// Write subtotal, discount and printReceipt here
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int price = input.nextInt();
int quantity = input.nextInt();
// Call your methods here
}
}30 4
Subtotal: 120 Discount: 10 Total: 110
Hint 1
The parameters: subtotal(int price, int quantity), discount(int amount) and printReceipt(int amount, int off).
Hint 2
main stores the results of the first two methods in variables, then passes them to printReceipt.
Solution
import java.util.Scanner;
public class Receipt {
static int subtotal(int price, int quantity) {
return price * quantity;
}
static int discount(int amount) {
if (amount >= 100) {
return 10;
}
return 0;
}
static void printReceipt(int amount, int off) {
System.out.println("Subtotal: " + amount);
System.out.println("Discount: " + off);
System.out.println("Total: " + (amount - off));
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int price = input.nextInt();
int quantity = input.nextInt();
int amount = subtotal(price, quantity);
int off = discount(amount);
printReceipt(amount, off);
}
}A method can call itself with a smaller problem
This is called recursion, and the method is called recursive. There is no magic: each self-call is an ordinary call with its own frame and boxes, as with any method.
Countdown
Press "Debug line by line", then keep pressing "Step into". Watch the countDown() frames pile up on the stack, each with its own box n holding a different value, then watch them disappear one by one. Try countDown(5).
public class CountDown {
static void countDown(int n) {
if (n == 0) {
System.out.println("Go!");
} else {
System.out.println(n);
countDown(n - 1);
}
}
public static void main(String[] args) {
countDown(3);
}
}3 2 1 Go!
We moved the printing to after the self-call. What does it print?
public class CountUp {
static void countUp(int n) {
if (n > 0) {
countUp(n - 1);
System.out.println(n);
}
}
public static void main(String[] args) {
countUp(3);
}
}Show the output
1 2 3
Why? The frame with n = 3 waits for the later calls to finish, so it prints last. The first frame to reach the print is n = 1, then the waiting frames return in order. Check this in the debugger.
The base case stops the chain, and the recursive step moves toward it
A small problem whose answer we know without another call.
if (n <= 1) return 1;We solve the rest with the same method on a smaller problem.
n * factorial(n - 1)Factorial step by step
We split the calculation into two lines so the box smaller appears when each frame gets its answer back. Press "Debug line by line", then keep pressing "Step into", and write down what each frame returns. Try 5, then 13, and compare the second result with the correct value 6227020800: it has gone past what an int can hold.
public class Factorial {
static int factorial(int n) {
if (n <= 1) {
return 1;
}
int smaller = factorial(n - 1);
return n * smaller;
}
public static void main(String[] args) {
System.out.println(factorial(4));
}
}24
| Frame | On the way down it waits for | On the way back it returns |
|---|---|---|
factorial(4) | factorial(3) | 4 * 6 = 24 |
factorial(3) | factorial(2) | 3 * 2 = 6 |
factorial(2) | factorial(1) | 2 * 1 = 2 |
factorial(1) | Nothing: the base case | 1 |
A recursive sum
Write sumTo(n) using recursion and no loop: it returns 0 when n is 0, and otherwise returns n + sumTo(n - 1). main reads the number and prints the result.
import java.util.Scanner;
public class SumTo {
static int sumTo(int n) {
// Replace this line with the base case and the recursive step
return 0;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
System.out.println(sumTo(n));
}
}5
15
Hint 1
Start with the base case: if (n == 0).
Hint 2
You have no loop: the self-call does the repeating.
Solution
import java.util.Scanner;
public class SumTo {
static int sumTo(int n) {
if (n == 0) {
return 0;
}
return n + sumTo(n - 1);
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
System.out.println(sumTo(n));
}
}Without a base case the stack fills up and the program stops
Let us break the program on purpose. In the factorial lab above, delete the base case so the method looks like this, then press "Run", not "Debug".
static int factorial(int n) {
int smaller = factorial(n - 1);
return n * smaller;
}The output starts with Exception in thread "main" java.lang.StackOverflowError, followed by a long list repeating the same line from factorial. Each of those lines is a call that has not finished.
What is wrong with countDown if the self-call is countDown(n + 1), the base case is n == 0, and it starts with countDown(3)?
n moves away from 0 with every call.isPrime is a tool you write once and keep using
Checking prime numbers
A prime number is greater than 1 and divides only by 1 and itself. Notice that return false ends the loop as soon as a divisor is found. Change the limit to 50, then add a line that prints isPrime(1).
public class PrimeList {
static 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 static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
if (isPrime(i)) {
System.out.print(i + " ");
}
}
System.out.println();
}
}2 3 5 7 11 13 17 19
Count the primes
Read n, and use isPrime as it is to count the primes from 1 to n. Print Primes: followed by the count.
import java.util.Scanner;
public class CountPrimes {
static 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 static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
// Count the primes from 1 to n, then print the result
}
}10
Primes: 4
Hint 1
Do not change isPrime. A loop, a counter and if (isPrime(i)) in main are enough.
Solution
import java.util.Scanner;
public class CountPrimes {
static 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 static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int count = 0;
for (int i = 1; i <= n; i++) {
if (isPrime(i)) {
count++;
}
}
System.out.println("Primes: " + count);
}
}gcd and power build solutions to bigger problems
Two new tools
gcd works out the greatest common divisor by Euclid's method. The method changed its parameters, so did x change in main? Check with the debugger. power returns a long so that large results fit as exact whole numbers.
public class MathTools {
static int gcd(int a, int b) {
while (b != 0) {
int remainder = a % b;
a = b;
b = remainder;
}
return a;
}
static long power(int base, int exponent) {
long result = 1;
for (int i = 0; i < exponent; i++) {
result = result * base;
}
return result;
}
public static void main(String[] args) {
int x = 84;
System.out.println(gcd(x, 36));
System.out.println(x);
System.out.println(power(2, 10));
System.out.println(power(2, 40));
System.out.println(power(7, 0));
}
}12 84 1024 1099511627776 1
Least common multiple
Write lcm(a, b) using gcd, knowing that lcm equals a / gcd(a, b) * b. Read two numbers and print LCM: followed by the result.
import java.util.Scanner;
public class Lcm {
static int gcd(int a, int b) {
while (b != 0) {
int remainder = a % b;
a = b;
b = remainder;
}
return a;
}
// Write the lcm method here
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int a = input.nextInt();
int b = input.nextInt();
// Print the result here
}
}4 6
LCM: 12
Hint 1
The body is one line, and it calls gcd.
Solution
import java.util.Scanner;
public class Lcm {
static int gcd(int a, int b) {
while (b != 0) {
int remainder = a % b;
a = b;
b = remainder;
}
return a;
}
static int lcm(int a, int b) {
return a / gcd(a, b) * b;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int a = input.nextInt();
int b = input.nextInt();
System.out.println("LCM: " + lcm(a, b));
}
}A sum of powers
Read base and n, then use power to add up the powers from base to the 0 up to base to the n. Print Sum: followed by the result. Example: 2 and 3 give 1 + 2 + 4 + 8, so Sum: 15.
import java.util.Scanner;
public class SumOfPowers {
static long power(int base, int exponent) {
long result = 1;
for (int i = 0; i < exponent; i++) {
result = result * base;
}
return result;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int base = input.nextInt();
int n = input.nextInt();
// Add up the powers here, then print the result
}
}2 3
Sum: 15
Hint 1
The sum must be a long, or the last test will not fit.
Hint 2
The loop runs from 0 up to and including n.
Solution
import java.util.Scanner;
public class SumOfPowers {
static long power(int base, int exponent) {
long result = 1;
for (int i = 0; i < exponent; i++) {
result = result * base;
}
return result;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int base = input.nextInt();
int n = input.nextInt();
long sum = 0;
for (int i = 0; i <= n; i++) {
sum = sum + power(base, i);
}
System.out.println("Sum: " + sum);
}
}Review questions
Which pair of methods is valid overloading?
What is the base case in a recursive method?
Put today's ideas together: what does this print?
public class ReviewValue {
static int bump(int n) {
n++;
return n;
}
static int bump(int n, int by) {
n += by;
return n;
}
public static void main(String[] args) {
int n = 1;
bump(n);
System.out.println(n);
n = bump(n, 5);
System.out.println(n);
}
}Show the output
1 6
Why? The first call changes a copy and we ignore what it returns, so n stays 1. For the second, Java picks the two-parameter version, and we assign what it returns to n, making it 6.