01 · Java basics — note 8 of 12
Methods
Break logic into reusable blocks, pass values safely, and trace execution on the call stack.
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
- Writing every calculation inside a single continuous block produces repetitive code that is difficult to read and update.
- Grouping related statements into named subroutines allows you to reuse identical logic across multiple parts of your program without copying lines.
- Isolating variables inside separate method bodies prevents calculations in one routine from unintentionally modifying variables in another.
- 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
staticindicates 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, orboolean(from variables and primitive types). If the method performs actions without producing a value, its return type isvoid. - Method name: An identifier written in camelCase starting with a lowercase letter (such as
calculateAreaorfindTarget). - 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
returnkeyword followed by an expression matching the declared return type. When execution hitsreturn, the method immediately exits and delivers the value back to the caller.
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);
}
}Rectangle area: 40In this program:
calculateRectangleAreais a method that accepts two integer parameters,widthandheight, and returns their product as anint.areais a local variable insidecalculateRectangleAreastoring the result ofwidth * height.roomWidthandroomHeightare variables insidemaininitialised to 5 and 8.totalAreareceives the value 40 returned bycalculateRectangleAreaand passes it toSystem.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:
- It pauses execution of the current method at the call site.
- It evaluates the arguments and passes them into the called method.
- It runs the statements inside the called method from top to bottom.
- When the called method executes a
returnstatement, 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.
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);
}
}Square of 4: 16
Difference: 16In this program:
squareaccepts one integer input namedvalueand returnsvalue * value.squaredNumberstores the result ofsquare(number), which evaluates to 16.differenceevaluates two independent method calls (square(5)which produces 25, andsquare(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.
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);
}
}Good morning
Invalid hourIn this program:
printGreetingtakes an integerhourOfDayand has avoidreturn type.- When
hourOfDayis 25, the validation check triggers, printsInvalid hour, and executesreturn;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 amountandint percent). - Arguments (actual arguments): The concrete values or evaluated expressions supplied between the parentheses at the call site (such as
500and10).
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.
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);
}
}Calculated tax: 50
Original amount: 500In this program:
computeTaxdeclares parametersamountandpercent.- The
mainmethod also declares local variables namedamountandpercent. - Even though both methods use the identifiers
amountandpercent, 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:
- 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. - 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.
- Modifying an element (
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]);
}
}Inside incrementPrimitive: 11
Caller originalValue: 10
Caller scores[0] after update: 99
Caller scores[0] after reassign: 99In this program:
incrementPrimitiveadds 1 to its local parametercounter. The caller's variableoriginalValuestays 10.updateArrayElementchanges index 0 of the array through the copied reference, changingscores[0]from 10 to 99.reassignArrayParameterassigns a brand new array to its local parameternumbers. The caller's variablescorescontinues to reference the original array, soscores[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:
- The method name.
- 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)versusadd(int a, int b, int c). - Types of parameters: e.g.
add(int a, int b)versusadd(double a, double b). - Order of parameter types: e.g.
format(int a, double b)versusformat(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:
Main.java:6: error: method calculate() is already defined in class Main
static double calculate() {
^
1 errorHere is a program demonstrating valid method overloading:
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);
}
}Sum of two ints: 30
Sum of three ints: 60
Sum of two doubles: 7.0In 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:
- A method can have at most one varargs parameter.
- The varargs parameter must be the last parameter in the parameter list.
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);
}
}Zero arguments: 0
Two arguments: 30
Four arguments: 10
Existing array: 30In this program:
sumAllacceptsint... numbers. CallingsumAll()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:
Main.java:4: error: variable count is already defined in method test(int)
int count = 10;
^
1 errorHowever, sibling blocks that do not enclose each other can each declare variables with the same name without any error:
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);
}
}Final bonus: 120In this program:
extraMultiplieris declared inside theifblock. Its scope is restricted to that block; attempting to accessextraMultiplierafter the closing brace of theifstatement causes a compile error.bonusis declared in the outer method block, so it remains accessible throughoutcalculateScopedBonus.
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:
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);
}
}Final total: 210Step 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.
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
inttolong, orfloattodouble). 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:
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);
}
}Phase 1 widening to long: 5In this program:
inspect(long)requires widening 32-bitintto 64-bitlongwithout 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:
findMaxfinds 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.scaleElementsmultiplies every element of an array by a factor in place, taking advantage of reference pass-by-value.calculateAveragecomputes the average score using a 64-bit accumulator to prevent integer overflow.
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);
}
}Highest score: 88
Clamped above: 100
Clamped range: 0
Scaled first score: 30
Average scaled score: 87Traps
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
valinside 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; }Reassigning an array parameter reference
Assigning a new array to parameter
arronly 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 useArrays.fill.✕ WRONGstatic void reset(int[] arr) { arr = new int[arr.length]; }✓ RIGHTstatic void reset(int[] arr) { Arrays.fill(arr, 0); }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 ifchain leaves any scenario uncovered (such asval == 0), compilation fails with the errormissing 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;Writing unreachable code after a return statement
Statements placed directly after an unconditional
returnstatement 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;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
inttolong), the compiler cannot pick a most specific method. It halts compilation withreference to run is ambiguous. Disambiguate the call by explicitly casting or using literal suffixes like5L.✕ WRONGstatic void run(int a, long b) {} static void run(long a, int b) {} run(5, 5);✓ RIGHTrun(5, 5L);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() {}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); }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) { ... }