Lesson 10 of 25

Methods

Anatomy of a Method

A method is a named block of code you can run from elsewhere. Its purpose is not merely to avoid retyping — it is to give a name to an idea. When a chunk of logic is called calculatePercentage, the reader of the calling code does not have to understand how it works to understand what is happening. That is the real payoff, and it is why a method with a good name is worth writing even if it is used only once.

Every method declaration has the same five parts, in the same order: modifiers, return type, name, parameter list, and body. public static int add(int a, int b) { ... } reads as: usable from anywhere (public), belongs to the class rather than to an object (static), gives back an int, is called add, and takes two int values.

The return type is the part beginners get wrong most. It says what kind of value the method hands back to whoever called it. If a method computes something, it should return it rather than print it — printing throws the answer away where nobody else can use it. Write void only when the method genuinely produces no value, such as one whose whole job is to display a menu.

Parameters are the inputs, declared with a type and a name, and they exist only inside the method. The values you supply when calling are called arguments. Note that Java has no default parameter values — you cannot write int add(int a, int b = 0). The Java answer to that need is overloading, covered further down.

Example
public class Calculator {

    //  modifiers    return type
    //     |             |     name    parameters
    //  ---------       ---    ---   ------------------
    public static      int    add   (int a, int b) {
        return a + b;                       // hands the value back
    }

    // void: does something, returns nothing
    public static void greet(String name) {
        System.out.println("Hello, " + name + "!");
    }

    // Compute and RETURN — do not print from inside
    public static double percentage(int scored, int total) {
        return (scored * 100.0) / total;    // 100.0 forces decimal division
    }

    public static void main(String[] args) {
        int sum = add(5, 3);
        System.out.println(sum);                    // 8

        greet("Ananya");                            // Hello, Ananya!

        double pct = percentage(430, 500);
        System.out.printf("Percentage: %.2f%%%n", pct);   // Percentage: 86.00%

        // Because it returns a value, it can be used inside other expressions
        if (percentage(430, 500) >= 75) {
            System.out.println("Distinction");
        }
    }
}
Notes
  • A method that returns a value must return one on every possible path. If an if returns but the else falls off the end, the compiler says missing return statement. Adding a final unconditional return is usually the honest fix; adding return 0; just to silence it usually hides a real gap in your logic.

Arguments Are Copies — Even When They Are References

Java is pass-by-value, with no exceptions. Whenever you call a method, each argument's value is copied into the parameter. What confuses people is what "the value" is when the argument is an object.

For a primitive, the value is the number itself. Change the parameter inside the method and the caller's variable is untouched, because the method is working on a copy. This is why a swap(int a, int b) method cannot work in Java, however it is written.

For an object, the variable does not hold the object — it holds a reference, an arrow pointing at it. So the copy that is passed in is a copy of the arrow, and both arrows point at the same object. That means the method can reach through and change the object's contents, and the caller sees those changes. But if the method reassigns its parameter to a different object, it has only redirected its own copy of the arrow, and the caller sees nothing.

The rule that captures both cases: a method can change what an object contains, but it can never change which object the caller's variable points at. Once that clicks, the behaviour of arrays, ArrayLists and your own classes when passed to methods stops being mysterious.

Example
import java.util.*;

public class PassByValue {

    static void tryToChange(int x) {
        x = 100;                    // changes the local copy only
    }

    static void addItem(List<String> list) {
        list.add("added");          // reaches through the arrow — caller SEES this
    }

    static void tryToReplace(List<String> list) {
        list = new ArrayList<>();   // redirects the local arrow only
        list.add("invisible");
    }

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

        List<String> items = new ArrayList<>();
        items.add("first");

        addItem(items);
        System.out.println(items);          // [first, added]

        tryToReplace(items);
        System.out.println(items);          // [first, added] — still
    }
}

// Strings look like an exception but are not: they are immutable,
// so a method has no way to change the contents at all.
static void shout(String s) {
    s = s.toUpperCase();      // makes a new String; the caller keeps the old one
}
Notes
  • The practical consequence: if a method needs to hand back a modified value, return it. Relying on the caller's variable being changed for you works only for mutable objects and makes code much harder to follow.

Method Overloading

Overloading means defining several methods with the same name in the same class, distinguished by their parameter lists. Java decides which one to run by looking at the arguments at the call site. It is why System.out.println() works with an int, a String, a double or an object — there is a separate overload for each.

The parameter lists must differ in number or type of parameters. What does not count is the return type: two methods with identical parameters but different return types will not compile, because at a call site like add(2, 3); — used as a statement, with the result discarded — Java would have no way to tell which one you meant. Renaming a parameter does not count either.

Since Java has no default parameter values, overloading is how you provide them. Write the full version with every parameter, then write shorter versions that call it with sensible defaults. Keeping the real logic in exactly one place matters: if each overload has its own copy of the calculation, they will drift apart the first time someone fixes a bug in only one of them.

One warning worth having early: overloads that differ only between an int and an Integer, or between an int and a long, make it genuinely hard to predict which one is selected, because Java prefers widening over boxing over varargs. If two overloads could plausibly match the same call, give them different names instead.

Example
public class Overloading {

    // Different number of parameters
    static int add(int a, int b)        { return a + b; }
    static int add(int a, int b, int c) { return a + b + c; }

    // Different parameter types
    static double add(double a, double b) { return a + b; }
    static String add(String a, String b) { return a + b; }

    // NOT allowed — return type alone does not distinguish overloads
    // static double add(int a, int b) { return a + b; }

    // ---- Overloading as "default arguments" ----
    // The full version holds the only copy of the logic
    static double emi(double principal, double annualRate, int months) {
        double r = annualRate / 12 / 100;
        return (principal * r) / (1 - Math.pow(1 + r, -months));
    }

    // Shorter versions delegate to it
    static double emi(double principal, double annualRate) {
        return emi(principal, annualRate, 12);      // default: one year
    }

    public static void main(String[] args) {
        System.out.println(add(2, 3));           // 5   -> int version
        System.out.println(add(2, 3, 4));        // 9
        System.out.println(add(2.5, 3.5));       // 6.0 -> double version
        System.out.println(add("2", "3"));       // 23  -> String version

        System.out.printf("%.2f%n", emi(100000, 9.5));
    }
}
Notes
  • Overloading is resolved at compile time from the declared types of the arguments, not at run time from the actual objects. This distinction becomes important in the Polymorphism lesson, where method overriding is resolved the other way round.

Varargs: Any Number of Arguments

Sometimes you cannot know how many values a caller will supply. Writing five overloads of sum for two, three, four and five numbers is obviously wrong. Varargs — written as three dots after the type — let a method accept any number of arguments, including none.

Inside the method the parameter simply is an array, so you loop over it with a for-each and check .length. That is not an analogy; Java packages the arguments into a real array before the call. Which also means you may pass an existing array where varargs are expected, and it works.

Two rules constrain them. A varargs parameter must be the last parameter in the list, since otherwise Java could not tell where the variable-length part ends. And a method may have only one of them.

The gotcha to know: a varargs method can be called with zero arguments, so sum() compiles and returns 0. If your method has no sensible meaning with no arguments — a max function, for instance — declare it as max(int first, int... rest). That forces at least one argument at compile time, which is much better than throwing an exception at run time.

Example
public class Varargs {

    // Accepts zero or more ints
    static int sum(int... numbers) {
        int total = 0;
        for (int n : numbers) {     // 'numbers' is a real int[]
            total += n;
        }
        return total;
    }

    // Requires at least one argument
    static int max(int first, int... rest) {
        int best = first;
        for (int n : rest) {
            if (n > best) best = n;
        }
        return best;
    }

    // Varargs must come LAST
    static void log(String level, String... messages) {
        for (String m : messages) {
            System.out.println("[" + level + "] " + m);
        }
    }

    public static void main(String[] args) {
        System.out.println(sum(1, 2));            // 3
        System.out.println(sum(1, 2, 3, 4, 5));   // 15
        System.out.println(sum());                // 0  — legal!

        int[] marks = {90, 85, 78};
        System.out.println(sum(marks));           // 253 — an array works too

        System.out.println(max(4, 9, 2));         // 9
        // max();     // compile error — good, that is the point

        log("INFO", "Started", "Loading data", "Ready");
    }
}
Notes
  • You have been using varargs since your first program without noticing: String.format, System.out.printf and List.of are all varargs methods. That is why they accept any number of values after the format string.

static vs Instance, and Recursion

A static method belongs to the class itself and is called as ClassName.method(). An instance method belongs to an individual object and needs one to exist first: obj.method(). The rule that follows is short but produces the most confusing error a first-year student sees — non-static method cannot be referenced from a static context.

It happens because main is static. When your program starts, no object of your class exists, so a call to an instance method from inside main has no object to run on. Java reports this at compile time. The two honest fixes are to make the helper method static as well, if it does not need any object data, or to create an object in main and call the method on that. Marking everything static to make the error go away works for small exercises but stops working as soon as you write real classes.

The useful test is whether the method needs an object's own fields. Math.sqrt(x) needs nothing but its argument, so it is static. account.getBalance() reads the balance of one particular account, so it must be an instance method.

Finally, a method may call itself, which is called recursion. Every recursive method needs a base case that returns without recursing, and each call must move closer to it. Miss either and the calls pile up until the JVM throws StackOverflowError — an error, not an exception, and one you cannot sensibly catch. Recursion is essential for trees, graphs and divide-and-conquer algorithms; for plain counting a loop is clearer and uses no stack.

Example
public class StaticVsInstance {

    int instanceCounter = 0;

    void increment() {                 // instance method — uses a field
        instanceCounter++;
    }

    static int square(int n) {         // static — depends only on its argument
        return n * n;
    }

    public static void main(String[] args) {
        System.out.println(square(5));         // 25 — fine, both are static

        // increment();
        // error: non-static method increment() cannot be referenced
        //        from a static context

        StaticVsInstance obj = new StaticVsInstance();   // make an object...
        obj.increment();                                  // ...then call on it
        System.out.println(obj.instanceCounter);          // 1
    }
}

// ---- Recursion ----
static long factorial(int n) {
    if (n <= 1) return 1;              // base case — stops the recursion
    return n * factorial(n - 1);       // moves toward the base case
}
// factorial(5) = 5 * 4 * 3 * 2 * 1 = 120

// Missing base case -> StackOverflowError
// static int bad(int n) { return bad(n - 1); }
Notes
  • Java does not allow one method to be declared inside another. If a piece of logic is only needed in one place, it still becomes a separate method in the same class — usually a private one, so the rest of the program is not tempted to call it.
Ask AI