University College of Applied SciencesProgramming 1
العربية
Lecture 12 · 100 min

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

  1. Write methods that share a name, and work out which one Java picks in a call
  2. Explain why a method does not know the variables of main
  3. Prove with the debugger that primitives are passed by value
  4. Break a long program into small methods, each with a clear name and one responsibility
  5. Trace a method that calls itself and identify its base case
  6. Reuse isPrime, gcd and power in new programs
In this lecture
  1. Several methods can share a name if their parameters differ
  2. Java picks the method from the number and types of the arguments
  3. Each version must clearly differ in its parameters
  4. A variable lives only inside the method that declares it
  5. A method receives a copy of the value, not the variable itself
  6. The swap method fails, and the debugger shows why
  7. A program written entirely in main is hard to follow
  8. Decomposition changes the shape of a program, not its behaviour
  9. A good name describes one job
  10. A method can call itself with a smaller problem
  11. The base case stops the chain, and the recursive step moves toward it
  12. Without a base case the stack fills up and the program stops
  13. isPrime is a tool you write once and keep using
  14. gcd and power build solutions to bigger problems
  15. Review questions
Part one: how calls work

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));
    }
}
Output
7
7.5
6

Java picks the method from the number and types of the arguments

  1. Rule out any with the wrong number of parameters
  2. Prefer an exact match of the types
  3. Otherwise accept a conversion to a wider typeSuch as char to int and int to double. 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
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
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);?

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));
    }
}
Compiler messageis already defined in class
Show 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));
    }
}
Output
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));
    }
}
Compiler messagereference to total is ambiguous
Show 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));
    }
}
Output
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();
    }
}
Compiler messagecannot find symbol
Show 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);
    }
}
Output
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
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
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);
    }
}
Output
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);
    }
}
Output
in swap: a = 9, b = 5
in main: a = 5, b = 9
  1. Press "Debug line by line", then "Next line" until swap(a, b);The main() frame has boxes a = 5 and b = 9.
  2. Press "Step into"The frame swap() appears above main(), with two more boxes that have the same names and values.
  3. Press "Next line" until the } of swapThe values swap in the swap() frame, while the main() frame stays as it was: a = 5 and b = 9.
  4. 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?

A good name describes one job

Weak nameBetter nameWhy
doStuffcalculateAverageA method does something, so start its name with a verb that says what
checkisPrimeA boolean method reads as a question
readAndPrintTotalreadInt, printTotalThe 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

    }
}
stdin
30
4
Expected output
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);
    }
}
Part three: recursion and reusable tools

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);
    }
}
Output
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
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

Base case

A small problem whose answer we know without another call.

if (n <= 1) return 1;
Recursive step

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));
    }
}
Output
24
FrameOn the way down it waits forOn 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 case1

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));
    }
}
stdin
5
Expected output
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;
}
Try it in the factorial lab, then put the base case back.

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)?

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();
    }
}
Output
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

    }
}
stdin
10
Expected output
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));
    }
}
Output
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

    }
}
stdin
4
6
Expected output
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

    }
}
stdin
2
3
Expected output
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
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.