Java Methods
A method in Java is a named block of code that performs a specific task and can be called (invoked) whenever you need that task done, as many times as you like. Methods let you break a large program into smaller, reusable, testable pieces, avoid duplicating logic, and give meaningful names to steps in your program’s flow. Nearly everything you do in Java, from main itself to library calls like System.out.println, happens through methods, so understanding how they are declared, how parameters and return values behave, and how the JVM actually executes a call is fundamental to writing real Java programs.
Overview: How Methods Work
A Java method is defined once, inside a class, and can then be invoked from other code as many times as needed. Every method has a signature, its name plus the number and types of its parameters, and a body, the block of statements between curly braces that runs each time the method is called. A method also declares a return type: either a specific type (like int, String, or a class) that it hands back to the caller with a return statement, or the keyword void if it performs an action but returns nothing.
Most methods you write early on will be static methods, declared inside a class but not tied to any particular object of that class. You call them directly on the class name, or, from inside the same class, simply by name, e.g. Main.add(2, 3) or just add(2, 3). Later, once you learn about objects, you will also write instance methods that belong to a specific object and can read and modify that object’s fields. This lesson focuses on static methods, since main itself is static and calls other static methods directly.
When a method is called, the JVM allocates a new stack frame on the call stack for that invocation. The frame holds the method’s parameters and local variables. Java parameters are always passed by value: for primitives (like int or double) a copy of the value is placed in the new frame, so changes to the parameter inside the method never affect the caller’s variable. For objects, the value that gets copied is the reference (the address) to the object, so the method can use that reference to modify the object’s internal state, but reassigning the parameter itself to point at a different object has no effect on the caller’s reference. This distinction trips up many beginners and is covered in the Common Mistakes section below.
Java also supports method overloading: several methods in the same class can share a name as long as their parameter lists differ in number or type. The compiler decides which overload to call based on the argument types at the call site, entirely at compile time, which is why overloading is sometimes called compile-time polymorphism, as opposed to the runtime polymorphism you meet later with inheritance and overriding.
Syntax
The general form of a static method declaration is:
modifiers returnType methodName(parameterType1 paramName1, parameterType2 paramName2) {
// method body: statements that run when the method is called
return value; // required unless returnType is void
}
| Part | Meaning |
|---|---|
modifiers |
Keywords such as public, private, and static that control visibility and whether the method belongs to the class itself rather than an instance. |
returnType |
The type of value the method sends back to its caller, or void if it returns nothing. |
methodName |
An identifier following Java naming conventions: lowercase first letter, camelCase for multiple words, e.g. calculateTotal. |
| parameter list | Zero or more comma-separated inputs the method needs. An empty parameter list is written as (). |
| method body | The statements executed on each call, enclosed in braces. |
return |
Exits the method immediately and, for non-void methods, sends a value back to the caller. |
Examples
Example 1: A basic method with parameters and a return value
public class Main {
static int add(int a, int b) {
return a + b;
}
static void greet(String name) {
System.out.println("Hello, " + name + "!");
}
public static void main(String[] args) {
greet("Maya");
int sum = add(5, 7);
System.out.println("5 + 7 = " + sum);
}
}
Output:
Hello, Maya!
5 + 7 = 12
greet is a void method: it prints something but returns nothing, so it’s called as a standalone statement. add returns an int, so its result can be stored in a variable. Notice that a and b inside add are entirely separate local variables from anything in main; they only receive copies of the values 5 and 7.
Example 2: Method overloading
public class Main {
static int multiply(int a, int b) {
return a * b;
}
static double multiply(double a, double b) {
return a * b;
}
static int multiply(int a, int b, int c) {
return a * b * c;
}
public static void main(String[] args) {
System.out.println(multiply(3, 4));
System.out.println(multiply(2.5, 4.0));
System.out.println(multiply(2, 3, 5));
}
}
Output:
12
10.0
30
There are three methods named multiply here, and the compiler picks the correct one based on the number and types of the arguments at each call site: two ints pick the first version, two doubles pick the second, and three ints pick the third. This resolution happens entirely at compile time, before the program ever runs.
Example 3: Recursion (a method calling itself)
public class Main {
static long factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
System.out.println(i + "! = " + factorial(i));
}
}
}
Output:
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
A recursive method calls itself with a smaller version of the problem until it reaches a base case (here, n <= 1) that stops the recursion. Every call to factorial gets its own stack frame with its own copy of n, so factorial(3) waits for factorial(2), which waits for factorial(1), before multiplying the results back up the chain.
How It Works Step by Step (Under the Hood)
Follow what happens when main calls factorial(3) from Example 3:
- The JVM pushes a new stack frame for
factorial(3), storing the parametern = 3. - Since
3 <= 1is false, the method executesreturn 3 * factorial(2). To evaluate this it must first callfactorial(2), so a second frame is pushed on top withn = 2. The first frame is paused, not discarded, it remains on the stack waiting for a result. - The same thing happens again:
factorial(2)callsfactorial(1), pushing a third frame withn = 1. factorial(1)hits the base case (1 <= 1is true) and returns1immediately, popping its frame off the stack.factorial(2)resumes with that result, computes2 * 1 = 2, returns2, and its frame is popped.factorial(3)resumes, computes3 * 2 = 6, returns6, and its frame is popped, leaving only the originalmainframe.
This push-and-pop behavior is why deep or infinite recursion (a method that never reaches its base case) eventually throws a StackOverflowError: each pending call keeps its frame alive on a call stack of limited size. It’s also why parameters behave the way they do: each frame has its own independent copies of its parameters and local variables, completely isolated from every other frame, including other calls to the very same method.
Common Mistakes
Mistake 1: Not returning a value on every possible path
A non-void method must return a value along every path through its body, or the code will not compile:
static int classify(int score) {
if (score >= 90) {
return 1;
} else if (score >= 70) {
return 2;
}
// missing return here when score < 70 -- this will not compile:
// error: missing return statement
}
The compiler cannot prove that one of the two if branches always runs, so it treats “falling off the end” of the method as a missing return. The fix is to make sure every branch, including a final else, returns a value:
public class Main {
static int classify(int score) {
if (score >= 90) {
return 1;
} else if (score >= 70) {
return 2;
} else {
return 3;
}
}
public static void main(String[] args) {
System.out.println(classify(95));
System.out.println(classify(75));
System.out.println(classify(50));
}
}
Output:
1
2
3
Mistake 2: Expecting a method to modify the caller’s primitive variables
Because Java passes primitives by value, this “swap” method compiles fine but does nothing useful:
public class Main {
static void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
public static void main(String[] args) {
int a = 3, b = 9;
swap(a, b);
System.out.println("a = " + a + ", b = " + b);
}
}
Output:
a = 3, b = 9
swap only rearranges its own local copies x and y; a and b in main are untouched. To actually get two values back out of a method, return them, for example bundled in an array (or, in real code, in a small object):
public class Main {
static int[] swap(int x, int y) {
return new int[] { y, x };
}
public static void main(String[] args) {
int a = 3, b = 9;
int[] result = swap(a, b);
a = result[0];
b = result[1];
System.out.println("a = " + a + ", b = " + b);
}
}
Output:
a = 9, b = 3
Best Practices
- Give each method a single, clear responsibility; if you struggle to name it without using “and”, split it into two methods.
- Use descriptive, verb-based names in camelCase, such as
calculateTotalorisValidEmail, so the call site reads like a sentence. - Keep parameter lists short. If a method needs many related values, consider grouping them into a class rather than passing five or six loose parameters.
- Prefer returning a value over mutating shared state when possible; pure methods (same input always gives the same output, no side effects) are far easier to test and reason about.
- Always ensure every branch of a non-void method returns a value, and let the compiler catch you if you forget.
- Reach for overloading only when the different versions truly do the “same conceptual thing” with different inputs; otherwise give them distinct names.
- For recursive methods, always identify the base case first and double-check that every recursive call moves strictly closer to it.
Practice Exercises
1. Write a static method isEven(int n) that returns true if n is even and false otherwise, then call it from main for the numbers 4, 7, and 10.
2. Write two overloaded methods named max: one that takes two int values and one that takes two double values, each returning the larger of the two.
3. Write a recursive method sumDigits(int n) that returns the sum of the digits of a positive integer (for example, sumDigits(1234) should return 10). Identify the base case before you start coding.
Summary
- A method groups reusable code under a name, with a signature (name plus parameter types), a body, and a return type or
void. - Static methods belong to the class itself and are called by name or via the class name, without needing an object.
- Java always passes arguments by value; for objects, the reference is copied, not the object itself.
- Each method call gets its own stack frame with independent parameters and local variables, which is why recursion works and why deep recursion can overflow the stack.
- Overloading lets multiple methods share a name if their parameter lists differ; the compiler resolves which one to call at compile time.
- Non-void methods must return a value on every possible execution path.
