Skip to content
elephantoo

Methods

Lesson 7 of 43 16 min read

Parameters, return values, overloading, varargs, pass-by-value, scope and recursion.


As programs grow, you do not want one giant main. A method is a named, reusable block of code that performs one task. You have already used many: System.out.println, Math.max, name.length(). Now you will write your own.

Methods let you:

  • Reuse logic instead of copy-pasting it.
  • Name ideas (calculateTax says more than ten lines of arithmetic).
  • Test small pieces independently.

Defining and calling a method#

Greeter.java
public class Greeter {
    static void greet(String name) {
        System.out.println("Hello, " + name + "!");
    }

    public static void main(String[] args) {
        greet("Asha");
        greet("Ben");
    }
}
Output
Hello, Asha!
Hello, Ben!

Anatomy of a method declaration:

Output
static  void   greet  (String name)   { ... }
  |      |       |          |            |
  |   return   name    parameter list   body
modifier type
  • static means the method belongs to the class, so main (also static) can call it directly. You will learn about instance methods in the OOP lessons.
  • The return type says what the method gives back; void means nothing.
  • Parameters are inputs, each with a type and a name. The values you pass when calling are the arguments.

Returning values#

A method with a non-void return type must return a value of that type on every path:

Circle.java
public class Circle {
    static double area(double radius) {
        return Math.PI * radius * radius;
    }

    static boolean isEven(int n) {
        return n % 2 == 0;
    }

    static int max(int a, int b) {
        if (a > b) {
            return a;
        }
        return b;
    }

    public static void main(String[] args) {
        double a = area(2.0);
        System.out.printf("Area: %.2f%n", a);
        System.out.println(isEven(10) + " " + isEven(7));
        System.out.println(max(3, 8));
        System.out.println(max(max(4, 9), 6));   // calls can be nested
    }
}
Output
Area: 12.57
true false
8
9

return ends the method immediately. That enables guard clauses: handle special cases first and return early, keeping the main logic unindented:

Java
static String classify(int age) {
    if (age < 0) return "invalid";
    if (age < 13) return "child";
    if (age < 20) return "teen";
    return "adult";
}

Parameters are passed by value#

Java always passes a copy of each argument into the method. For primitives this means the method cannot change the caller's variable:

PassByValue.java
import java.util.Arrays;

public class PassByValue {
    static void tryToChange(int x) {
        x = 99;                 // changes the local copy only
    }

    static void changeArray(int[] arr) {
        arr[0] = 99;            // modifies the object the copy points to
    }

    static void replaceArray(int[] arr) {
        arr = new int[]{7, 7};  // re-points the local copy only
    }

    public static void main(String[] args) {
        int n = 5;
        tryToChange(n);
        System.out.println(n);

        int[] data = {1, 2, 3};
        changeArray(data);
        System.out.println(Arrays.toString(data));

        replaceArray(data);
        System.out.println(Arrays.toString(data));
    }
}
Output
5
[99, 2, 3]
[99, 2, 3]

For objects and arrays, the value copied is the reference (the address). Both copies point to the same array, so changes to its contents are visible to the caller. But assigning a new array to the parameter only changes the local copy. This distinction, "pass-by-value of references", is a favourite interview question.

Method overloading#

Several methods can share a name if their parameter lists differ in number, type or order of parameters:

Overloading.java
public class Overloading {
    static int add(int a, int b) {
        return a + b;
    }

    static int add(int a, int b, int c) {
        return a + b + c;
    }

    static double add(double a, double b) {
        return a + b;
    }

    static String add(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {
        System.out.println(add(2, 3));
        System.out.println(add(2, 3, 4));
        System.out.println(add(2.5, 1.5));
        System.out.println(add("Ele", "phantoo"));
    }
}
Output
5
9
4.0
Elephantoo

The compiler picks the best match from the argument types at compile time. The return type alone cannot distinguish overloads.

Varargs: any number of arguments#

Declare Type... name as the last parameter to accept zero or more values. Inside the method it is an array:

Varargs.java
public class Varargs {
    static int sum(int... numbers) {
        int total = 0;
        for (int n : numbers) {
            total += n;
        }
        return total;
    }

    static void log(String level, String... parts) {
        System.out.println("[" + level + "] " + String.join(" ", parts));
    }

    public static void main(String[] args) {
        System.out.println(sum());
        System.out.println(sum(5));
        System.out.println(sum(1, 2, 3, 4));
        log("INFO", "Server", "started", "on", "port", "8080");
    }
}
Output
0
5
10
[INFO] Server started on port 8080

String.format, printf and List.of all use varargs.

Scope: where variables live#

A variable exists only inside the block { } where it is declared:

Java
static void demo() {
    int x = 10;              // visible in the whole method
    if (x > 5) {
        int y = x * 2;       // visible only inside this if-block
        System.out.println(y);
    }
    // System.out.println(y);  // compile error: cannot find symbol y
}

Parameters and local variables are created fresh on each call and disappear when the method returns. Two methods can each have their own x without interfering.

Recursion: methods that call themselves#

A recursive method solves a problem by calling itself on a smaller version of the same problem. It needs:

  1. A base case that returns without recursing.
  2. A recursive case that moves towards the base case.
Recursion.java
public class Recursion {
    static long factorial(int n) {
        if (n <= 1) return 1;            // base case
        return n * factorial(n - 1);     // recursive case
    }

    static int fib(int n) {
        if (n < 2) return n;
        return fib(n - 1) + fib(n - 2);
    }

    static int sumDigits(int n) {
        if (n < 10) return n;
        return n % 10 + sumDigits(n / 10);
    }

    public static void main(String[] args) {
        System.out.println(factorial(5));
        System.out.println(factorial(20));
        System.out.println(fib(10));
        System.out.println(sumDigits(9045));
    }
}
Output
120
2432902008176640000
55
18

How factorial(4) unfolds:

Output
factorial(4) = 4 * factorial(3)
             = 4 * 3 * factorial(2)
             = 4 * 3 * 2 * factorial(1)
             = 4 * 3 * 2 * 1 = 24

Every call takes space on the call stack. Recurse too deeply, or forget the base case, and you get a StackOverflowError. Many recursive problems (factorial, sums) are simpler as loops; recursion shines for naturally recursive structures such as trees, folders and divide-and-conquer algorithms. Note that this naive fib recomputes the same values exponentially many times, so it becomes very slow for large n.

Designing good methods#

  • Do one thing. If you need "and" to describe it (validateAndSaveAndEmail), split it.
  • Name with verbs: calculateTotal, isValid, printReport. Boolean methods often start with is, has or can.
  • Keep them short. A method that fits on one screen is easier to understand.
  • Prefer returning values over printing inside the method; the caller can then decide what to do with the result.
  • Limit parameters. More than three or four is a hint to group them into an object.

A worked example#

Report.java
public class Report {
    static double average(int[] values) {
        if (values.length == 0) return 0;
        int sum = 0;
        for (int v : values) sum += v;
        return (double) sum / values.length;
    }

    static char grade(double avg) {
        if (avg >= 90) return 'A';
        if (avg >= 75) return 'B';
        if (avg >= 60) return 'C';
        return 'F';
    }

    static void printReport(String student, int... marks) {
        double avg = average(marks);
        System.out.printf("%-6s avg %.1f grade %c%n", student, avg, grade(avg));
    }

    public static void main(String[] args) {
        printReport("Riya", 92, 88, 95);
        printReport("Tom", 70, 65, 81);
        printReport("Li", 40, 55, 62);
    }
}
Output
Riya   avg 91.7 grade A
Tom    avg 72.0 grade C
Li     avg 52.3 grade F

%-6s prints a string left-aligned in 6 characters and %c prints a char.

Common mistakes#

  • Forgetting to use the return value: max(3, 8); on its own line computes 8 and throws it away.
  • A missing return on some path: the compiler reports missing return statement.
  • Expecting a method to change a primitive argument.
  • Calling an instance (non-static) method from static main without an object: non-static method cannot be referenced from a static context.

What's next#

Methods let you organise logic. Next we store many values in one variable with arrays.

Check your understanding

Quick quiz

0/3 answered
  1. 1.What does this print? static void bump(int x) { x = x + 1; } ... int n = 5; bump(n); System.out.println(n);

  2. 2.Which pair of methods is a valid overload?

  3. 3.What is missing from a recursive method that causes a StackOverflowError?

Finished reading?

Mark this lesson complete to track your progress.