algoz
Chapters/01 Java basics/Methods

Methods

Break logic into reusable blocks, pass values safely, and trace execution on the call stack.

Builds on
You'll gain
Split code into methods that workand trace execution on the call stack
Practice
Sheet coming sooncoding problems for this note
Summary

Methods group instructions into isolated, reusable routines that communicate through formal parameters and explicit return values while running on an ordered call stack.

The idea in real life

Think of a cook working at a busy food counter. Instead of chopping vegetables, simmering broth, and plating food all at the same time, the cook delegates specific jobs to prep stations. The cook hands a bowl of raw vegetables to the cutting station, waits for the chopped pieces to return, and adds them to the pan. Methods work the same way in code: your program hands data to an isolated helper block, the helper performs its task, and it returns the finished result to the exact line where you called it.

Why it exists

  1. Writing every calculation inside a single continuous block produces repetitive code that is difficult to read and update.
  2. Grouping related statements into named subroutines allows you to reuse identical logic across multiple parts of your program without copying lines.
  3. Isolating variables inside separate method bodies prevents calculations in one routine from unintentionally modifying variables in another.
  4. Breaking complex algorithms into small, single-purpose routines makes your code straightforward to test, trace, and verify.

How it works

Defining a method

Declare the name, inputs, return type, and body of a reusable routine.

In Java, every method must be declared inside a class container. As introduced in how a Java program runs, a method is a named block of code that performs a specific set of operations.

A standard static method declaration contains six distinct components:

  • Modifier: The keyword static indicates that the method belongs directly to the class container rather than an individual object. In chapter 02, you will learn how objects and instance methods work.
  • Return type: The data type of the value the method sends back to the caller, such as int, double, or boolean (from variables and primitive types). If the method performs actions without producing a value, its return type is void.
  • Method name: An identifier written in camelCase starting with a lowercase letter (such as calculateArea or findTarget).
  • Parameter list: Comma-separated input declarations enclosed in parentheses (...). Each parameter specifies its type and name. If a method requires no inputs, the parentheses remain empty ().
  • Method body: The block of statements enclosed in curly braces { ... } that run when the method is invoked.
  • Return statement: The return keyword followed by an expression matching the declared return type. When execution hits return, the method immediately exits and delivers the value back to the caller.
Main.java
public class Main {
    static int calculateRectangleArea(int width, int height) {
        int area = width * height;
        return area;
    }

    public static void main(String[] args) {
        int roomWidth = 5;
        int roomHeight = 8;
        int totalArea = calculateRectangleArea(roomWidth, roomHeight);
        System.out.println("Rectangle area: " + totalArea);
    }
}
Output
Rectangle area: 40

In this program:

  • calculateRectangleArea is a method that accepts two integer parameters, width and height, and returns their product as an int.
  • area is a local variable inside calculateRectangleArea storing the result of width * height.
  • roomWidth and roomHeight are variables inside main initialised to 5 and 8.
  • totalArea receives the value 40 returned by calculateRectangleArea and passes it to System.out.println.

Calling a method and using the result

Pass values to a method and receive its calculated output.

To invoke a method, you write its name followed by parentheses containing the input values you wish to send.

When the JVM encounters a method call:

  1. It pauses execution of the current method at the call site.
  2. It evaluates the arguments and passes them into the called method.
  3. It runs the statements inside the called method from top to bottom.
  4. When the called method executes a return statement, the JVM yields the returned value back to the call site and resumes the caller.

You can store the returned value in a variable, use it directly inside an arithmetic expression (from operators), or pass it immediately as an argument to another method call.

Main.java
public class Main {
    static int square(int value) {
        return value * value;
    }

    public static void main(String[] args) {
        int number = 4;
        int squaredNumber = square(number);
        int difference = square(5) - square(3);

        System.out.println("Square of 4: " + squaredNumber);
        System.out.println("Difference: " + difference);
    }
}
Output
Square of 4: 16
Difference: 16

In this program:

  • square accepts one integer input named value and returns value * value.
  • squaredNumber stores the result of square(number), which evaluates to 16.
  • difference evaluates two independent method calls (square(5) which produces 25, and square(3) which produces 9) and subtracts them to yield 16.

void methods

Execute a sequence of actions without returning a value.

Not every method needs to calculate and return a result. Methods that output text to the console, update shared state, or configure parameters declare their return type as void.

Inside a void method, you do not need a return statement. Execution finishes naturally when control reaches the closing curly brace } of the method body.

You can still use a bare return; statement without any expression inside a void method. This serves as an early exit, immediately stopping further statements in that method and returning control to the caller.

Main.java
public class Main {
    static void printGreeting(int hourOfDay) {
        if (hourOfDay < 0 || hourOfDay > 23) {
            System.out.println("Invalid hour");
            return;
        }

        if (hourOfDay < 12) {
            System.out.println("Good morning");
        } else {
            System.out.println("Good day");
        }
    }

    public static void main(String[] args) {
        printGreeting(9);
        printGreeting(25);
    }
}
Output
Good morning
Invalid hour

In this program:

  • printGreeting takes an integer hourOfDay and has a void return type.
  • When hourOfDay is 25, the validation check triggers, prints Invalid hour, and executes return; to exit early before reaching the greeting checks.

Parameters and arguments

Parameters receive copies of the values you pass when calling a method.

Although developers often use the words interchangeably, parameters and arguments represent two distinct parts of method communication:

  • Parameters (formal parameters): The variable names and types declared in the method definition header (such as int amount and int percent).
  • Arguments (actual arguments): The concrete values or evaluated expressions supplied between the parentheses at the call site (such as 500 and 10).

Parameter names are strictly local to the method that defines them. The caller can pass variables with different names, or even variables with the exact same names, without any conflict.

Main.java
public class Main {
    static int computeTax(int amount, int percent) {
        int tax = (amount * percent) / 100;
        return tax;
    }

    public static void main(String[] args) {
        int amount = 500;
        int percent = 10;
        int tax = computeTax(amount, percent);

        System.out.println("Calculated tax: " + tax);
        System.out.println("Original amount: " + amount);
    }
}
Output
Calculated tax: 50
Original amount: 500

In this program:

  • computeTax declares parameters amount and percent.
  • The main method also declares local variables named amount and percent.
  • Even though both methods use the identifiers amount and percent, each variable occupies its own independent storage slot in memory.

Going deeper

Pass-by-value

Java always passes arguments by copying their value into parameter variables.

Java is strictly pass-by-value for every data type (Java Language Specification, JLS §8.4.1). When you pass an argument to a method, Java copies the raw value of the argument into the method's parameter variable. Java never passes a direct reference to the caller's variable container.

This rule has two important consequences:

  1. For primitive types: The parameter receives a duplicate copy of the primitive bits (int, double, boolean, etc.). Modifying the parameter variable inside the method alters only that local copy. The caller's variable remains unchanged.
  2. For reference types (such as arrays): The value stored in an array variable is a reference: something that points to the array object on the heap (note 9 teaches arrays in depth). When you pass an array to a method, Java copies the reference value. Both the caller's variable and the parameter point to the exact same array on the heap. Therefore:
    • Modifying an element (numbers[0] = 99;) modifies the shared array on the heap, so the caller observes the change.
    • Reassigning the parameter variable (numbers = new int[]{ 500, 600 };) overwrites only the local parameter reference. The caller's variable still points to the original array.
Main.java
public class Main {
    static void incrementPrimitive(int counter) {
        counter = counter + 1;
        System.out.println("Inside incrementPrimitive: " + counter);
    }

    static void updateArrayElement(int[] numbers) {
        numbers[0] = 99;
    }

    static void reassignArrayParameter(int[] numbers) {
        numbers = new int[]{ 500, 600 };
    }

    public static void main(String[] args) {
        int originalValue = 10;
        incrementPrimitive(originalValue);
        System.out.println("Caller originalValue: " + originalValue);

        int[] scores = { 10, 20 };
        updateArrayElement(scores);
        System.out.println("Caller scores[0] after update: " + scores[0]);

        reassignArrayParameter(scores);
        System.out.println("Caller scores[0] after reassign: " + scores[0]);
    }
}
Output
Inside incrementPrimitive: 11
Caller originalValue: 10
Caller scores[0] after update: 99
Caller scores[0] after reassign: 99

In this program:

  • incrementPrimitive adds 1 to its local parameter counter. The caller's variable originalValue stays 10.
  • updateArrayElement changes index 0 of the array through the copied reference, changing scores[0] from 10 to 99.
  • reassignArrayParameter assigns a brand new array to its local parameter numbers. The caller's variable scores continues to reference the original array, so scores[0] remains 99.

Overloading

Share one method name across distinct parameter lists.

Method overloading (JLS §8.4.9) allows a class to declare multiple methods with the exact same name, as long as each version has a different parameter list.

A method's signature (JLS §8.4.2) consists exclusively of:

  1. The method name.
  2. The number, types, and order of its parameters.

Two methods overload successfully if their parameter lists differ by:

  • Number of parameters: e.g. add(int a, int b) versus add(int a, int b, int c).
  • Types of parameters: e.g. add(int a, int b) versus add(double a, double b).
  • Order of parameter types: e.g. format(int a, double b) versus format(double a, int b).

Return type alone cannot overload a method. The Java compiler uses the argument types at the call site to decide which method to invoke. Because you can call a method without assigning its result, the compiler cannot use the return type to disambiguate the call.

If you declare two methods with identical names and identical parameter lists but different return types:

text
Main.java:6: error: method calculate() is already defined in class Main
    static double calculate() {
                  ^
1 error

Here is a program demonstrating valid method overloading:

Main.java
public class Main {
    static int add(int firstNumber, int secondNumber) {
        return firstNumber + secondNumber;
    }

    static int add(int firstNumber, int secondNumber, int thirdNumber) {
        return firstNumber + secondNumber + thirdNumber;
    }

    static double add(double firstNumber, double secondNumber) {
        return firstNumber + secondNumber;
    }

    public static void main(String[] args) {
        int twoInts = add(10, 20);
        int threeInts = add(10, 20, 30);
        double twoDoubles = add(2.5, 4.5);

        System.out.println("Sum of two ints: " + twoInts);
        System.out.println("Sum of three ints: " + threeInts);
        System.out.println("Sum of two doubles: " + twoDoubles);
    }
}
Output
Sum of two ints: 30
Sum of three ints: 60
Sum of two doubles: 7.0

In this program:

  • add(int, int) adds two 32-bit integers.
  • add(int, int, int) adds three 32-bit integers.
  • add(double, double) adds two 64-bit floating-point numbers.
  • At each call site, the compiler selects the matching overload based on the argument count and types.

Varargs

Accept an arbitrary count of arguments packed into an array.

Java supports variable-arity parameters, commonly called varargs (JLS §8.4.1). By placing an ellipsis ... after the parameter type, you allow callers to pass zero, one, or many arguments of that type.

Behind the scenes, the Java compiler creates an array holding the supplied values and passes that array into the method. Inside the method body, a varargs parameter behaves identically to a standard array. For example, int... numbers is accessed inside the method as numbers.length and numbers[i].

Two strict rules govern varargs:

  1. A method can have at most one varargs parameter.
  2. The varargs parameter must be the last parameter in the parameter list.
Main.java
public class Main {
    static int sumAll(int... numbers) {
        int total = 0;
        for (int i = 0; i < numbers.length; i++) {
            total += numbers[i];
        }
        return total;
    }

    public static void main(String[] args) {
        int zeroCount = sumAll();
        int twoNumbers = sumAll(10, 20);
        int fourNumbers = sumAll(1, 2, 3, 4);

        int[] existingArray = { 5, 10, 15 };
        int arraySum = sumAll(existingArray);

        System.out.println("Zero arguments: " + zeroCount);
        System.out.println("Two arguments: " + twoNumbers);
        System.out.println("Four arguments: " + fourNumbers);
        System.out.println("Existing array: " + arraySum);
    }
}
Output
Zero arguments: 0
Two arguments: 30
Four arguments: 10
Existing array: 30

In this program:

  • sumAll accepts int... numbers. Calling sumAll() passes an empty array with length 0, returning 0.
  • sumAll(10, 20) automatically creates and passes a 2-element array.
  • sumAll(existingArray) accepts an already existing array directly without creating an extra wrapper array.

Local variables and scope

Variables declared inside a method exist only within their enclosing block.

A local variable is declared inside a method body or an inner block (such as a loop or conditional statement).

The scope of a local variable (JLS §6.3) is the region of code where that variable can be accessed by name. Scope begins at the point of declaration and ends at the closing curly brace } of its enclosing block. Once execution leaves that block, the variable goes out of scope and cannot be referenced.

Java forbids a local variable from shadowing another local variable or parameter of the same method (JLS §6.4). You cannot declare a local variable inside an inner block if an enclosing block or method parameter already uses that same identifier:

text
Main.java:4: error: variable count is already defined in method test(int)
            int count = 10;
                ^
1 error

However, sibling blocks that do not enclose each other can each declare variables with the same name without any error:

Main.java
public class Main {
    static int calculateScopedBonus(int baseScore) {
        int bonus = 0;

        if (baseScore > 50) {
            int extraMultiplier = 2;
            bonus = baseScore * extraMultiplier;
        }

        return bonus;
    }

    public static void main(String[] args) {
        int score = 60;
        int finalBonus = calculateScopedBonus(score);
        System.out.println("Final bonus: " + finalBonus);
    }
}
Output
Final bonus: 120

In this program:

  • extraMultiplier is declared inside the if block. Its scope is restricted to that block; attempting to access extraMultiplier after the closing brace of the if statement causes a compile error.
  • bonus is declared in the outer method block, so it remains accessible throughout calculateScopedBonus.

Under the hood

The call stack

Each method invocation allocates an isolated stack frame in memory.

At runtime, the Java Virtual Machine manages method execution through a private JVM stack allocated for each active thread (JVM Specification §2.5.2).

Whenever a thread invokes a method, the JVM allocates a new stack frame (JVM Specification §2.6) and pushes it onto the top of the call stack. A stack frame stores:

  • Local variable table: Holds method parameters and local variables. For a static method, parameter 1 occupies slot 0, parameter 2 occupies slot 1, and so on.
  • Operand stack: A push-down memory space where intermediate arithmetic calculations and bytecode operations push values and pop results.
  • Frame data: Bookkeeping information, including return addresses, references to the runtime constant pool, and exception dispatch tables.

When the active method completes—either by returning a value or throwing an unhandled exception—its stack frame is popped off the call stack and its memory is immediately reclaimed without requiring garbage collection. The caller's frame becomes the top of the stack again, resuming execution right where it paused.

Consider a three-level call chain where main invokes computeTotal, which in turn invokes calculateTax:

Main.java
public class Main {
    static int calculateTax(int amount) {
        int tax = (amount * 5) / 100;
        return tax;
    }

    static int computeTotal(int unitPrice, int count) {
        int subtotal = unitPrice * count;
        int tax = calculateTax(subtotal);
        return subtotal + tax;
    }

    public static void main(String[] args) {
        int price = 100;
        int quantity = 2;
        int finalTotal = computeTotal(price, quantity);
        System.out.println("Final total: " + finalTotal);
    }
}
Output
Final total: 210

Step through the same program below. Watch each call push a frame with its own copies of the arguments, and each return pop it and hand one value back. Change price or quantity to run it again with other numbers.

StackOverflowError

Uncontrolled nested calls consume all stack memory and crash the program.

The call stack has a fixed maximum size, set when the thread starts; the -Xss option of the java command changes it.

If a method calls itself without reaching a termination condition, each recursive call allocates and pushes another stack frame onto the thread's call stack. Because none of the calls finish and return, no frames are popped. Eventually, the stack runs out of memory, and the JVM halts with a java.lang.StackOverflowError.

Unlike typical bugs that throw a recoverable Exception, StackOverflowError is an instance of java.lang.Error. Errors signal critical conditions that normal applications should not attempt to catch.

text
Exception in thread "main" java.lang.StackOverflowError
	at Main.runawayRecursion(Main.java:3)
	at Main.runawayRecursion(Main.java:3)
	at Main.runawayRecursion(Main.java:3)

In chapter 07, you will study recursion properly, learning how to structure recursive algorithms with valid base cases that guarantee the call stack unwinds safely.

How Java picks an overload

The compiler resolves overloaded methods in three strict, sequential phases.

When a method call matches more than one overloaded method signature, the Java compiler determines the chosen target at compile time following the three-phase resolution process defined in JLS §15.12.2:

  • Phase 1: Identify matching arity methods applicable by subtyping and widening. The compiler searches for methods with the exact parameter count where arguments match through widening primitive conversions (such as widening int to long, or float to double). No autoboxing or varargs are permitted during this phase.
  • Phase 2: Identify matching arity methods applicable by method invocation conversion. If Phase 1 finds no applicable method, the compiler searches methods with matching parameter counts allowing autoboxing and unboxing (conversions between primitives and wrapper types; wrapper classes are covered in chapter 03).
  • Phase 3: Identify applicable variable arity methods. If Phase 2 finds no match, the compiler searches methods declared with varargs (...).

Because Phase 1 executes before Phase 3, primitive widening always takes precedence over varargs. If a class declares both inspect(long value) and inspect(int... values), calling inspect(5) with an int argument resolves to the long overload in Phase 1:

Main.java
public class Main {
    static void inspect(long value) {
        System.out.println("Phase 1 widening to long: " + value);
    }

    static void inspect(int... values) {
        System.out.println("Phase 3 varargs count: " + values.length);
    }

    public static void main(String[] args) {
        int number = 5;
        inspect(number);
    }
}
Output
Phase 1 widening to long: 5

In this program:

  • inspect(long) requires widening 32-bit int to 64-bit long without varargs packaging, which succeeds in Phase 1.
  • The compiler never advances to Phase 3, so inspect(int...) is not called.

Why every method here is static

Static methods belong to the class and require no object instances.

Throughout this chapter, every helper method is declared with the static modifier. A static method belongs to the class container itself rather than to an individual instance of that class.

The standard application entry point, public static void main, is static. In Java, static methods cannot directly invoke instance methods without first constructing an object using the new operator, because instance methods rely on an implicit this reference pointing to an object's instance state on the heap. Marking helper methods static allows main to call them directly without instantiating objects. In chapter 02, you will learn how classes instantiate objects and manage instance state.

In Java

Here is a complete program that brings these concepts together: calculating values with helper methods, overloading routines for flexible calling, modifying an array in place using pass-by-value references, and accumulating values safely with long arithmetic.

In this program:

  • findMax finds the largest value in an integer array.
  • clamp(int, int, int) restricts a number between explicit lower and upper bounds.
  • clamp(int, int) is an overloaded version that defaults the lower bound to 0.
  • scaleElements multiplies every element of an array by a factor in place, taking advantage of reference pass-by-value.
  • calculateAverage computes the average score using a 64-bit accumulator to prevent integer overflow.
Main.java
public class Main {
    // Returns the maximum integer found in the provided array.
    static int findMax(int[] numbers) {
        int highest = numbers[0];
        for (int i = 1; i < numbers.length; i++) {
            if (numbers[i] > highest) {
                highest = numbers[i];
            }
        }
        return highest;
    }

    // Returns value clamped between lowerLimit and upperLimit.
    static int clamp(int value, int lowerLimit, int upperLimit) {
        if (value < lowerLimit) {
            return lowerLimit;
        }
        if (value > upperLimit) {
            return upperLimit;
        }
        return value;
    }

    // Overloaded clamp that defaults lowerLimit to 0.
    static int clamp(int value, int upperLimit) {
        return clamp(value, 0, upperLimit);
    }

    // Multiplies each element of the given array by factor in place.
    static void scaleElements(int[] numbers, int factor) {
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = numbers[i] * factor;
        }
    }

    // Returns the integer average of the array values using long accumulation.
    static int calculateAverage(int[] numbers) {
        long sum = 0;
        for (int i = 0; i < numbers.length; i++) {
            sum += numbers[i];
        }
        return (int) (sum / numbers.length);
    }

    public static void main(String[] args) {
        int[] scores = { 15, 42, 88, 30 };

        int highestScore = findMax(scores);
        int clampedAbove = clamp(120, 100);
        int clampedRange = clamp(-5, 0, 50);

        scaleElements(scores, 2);
        int averageScore = calculateAverage(scores);

        System.out.println("Highest score: " + highestScore);
        System.out.println("Clamped above: " + clampedAbove);
        System.out.println("Clamped range: " + clampedRange);
        System.out.println("Scaled first score: " + scores[0]);
        System.out.println("Average scaled score: " + averageScore);
    }
}
Output
Highest score: 88
Clamped above: 100
Clamped range: 0
Scaled first score: 30
Average scaled score: 87

Traps

  1. Expecting primitive parameter changes to reach the caller

    Java passes all primitive arguments by value, copying the bits into the method's parameter variable. Modifying val inside the method modifies only that local copy, leaving the caller's variable untouched. Return the updated value and assign it at the call site instead.

    ✕ WRONGstatic void addFive(int val) { val += 5; }
    ✓ RIGHTstatic int addFive(int val) { return val + 5; }
  2. Reassigning an array parameter reference

    Assigning a new array to parameter arr only updates the local reference variable inside the method to point to the new array. The caller's reference continues to point to the original array in heap memory. To alter the caller's array, mutate its existing indices directly or use Arrays.fill.

    ✕ WRONGstatic void reset(int[] arr) { arr = new int[arr.length]; }
    ✓ RIGHTstatic void reset(int[] arr) { Arrays.fill(arr, 0); }
  3. Missing return statement along a conditional branch

    If a method declares a non-void return type, the Java compiler verifies that every possible execution path returns a value. If an if/else if chain leaves any scenario uncovered (such as val == 0), compilation fails with the error missing return statement.

    ✕ WRONGif (val > 0) return 1; else if (val < 0) return -1;
    ✓ RIGHTif (val > 0) return 1; else if (val < 0) return -1; else return 0;
  4. Writing unreachable code after a return statement

    Statements placed directly after an unconditional return statement can never execute. Under JLS §14.21, the Java compiler rejects this code with a compile error: unreachable statement. Place logging or cleanup statements before returning.

    ✕ WRONGreturn total; System.out.println(total);
    ✓ RIGHTSystem.out.println(total); return total;
  5. Ambiguous method overloads with primitive widening

    When calling an overloaded method where multiple methods match through widening (such as widening either the first or second int to long), the compiler cannot pick a most specific method. It halts compilation with reference to run is ambiguous. Disambiguate the call by explicitly casting or using literal suffixes like 5L.

    ✕ WRONGstatic void run(int a, long b) {} static void run(long a, int b) {} run(5, 5);
    ✓ RIGHTrun(5, 5L);
  6. Attempting to overload methods by return type alone

    In Java, a method's signature consists solely of its name and parameter types. The return type is not part of the signature. Declaring two methods with the same name and identical parameter lists but different return types causes a compile error: method is already defined. Give the methods distinct names or different parameter lists.

    ✕ WRONGstatic int getScore() {} static double getScore() {}
    ✓ RIGHTstatic int getIntScore() {} static double getDoubleScore() {}
  7. Recursive method calls without a base case

    A method that repeatedly invokes itself without a terminating condition pushes a new stack frame onto the call stack with every invocation. Because the call stack has a finite size, this quickly exhausts stack memory and throws StackOverflowError. Always define a clear base condition that stops further calls.

    ✕ WRONGstatic void repeat() { repeat(); }
    ✓ RIGHTstatic void repeat(int count) { if (count <= 0) return; repeat(count - 1); }
  8. Bundling multiple unrelated tasks into one method

    A method that parses input, computes values, and prints reports is difficult to debug, test, and reuse. Keep each method focused on a single task with clearly named inputs and a predictable return value.

    ✕ WRONGstatic void processUserInputAndComputeStatisticsAndSave()
    ✓ RIGHTstatic int computeStats(int[] nums) { ... }
AlgozSet in Geist & Geist MonoJava track