Java Methods
A method is a named, reusable block of code. Methods are how you avoid copy-pasting the same logic across your program: write it once, give it a name, and call it from anywhere. Java methods live inside classes — there are no top-level functions — and they may be static (belonging to the class) or instance (belonging to an object).
This tutorial covers method declaration, parameters, return values, overloading, varargs, and the modern features record methods and pattern-matching methods. By the end you will be able to design clean, well-named, well-structured methods.
1. Method Anatomy
A method declaration has six parts, though not all are required:
public static int add(int a, int b) {
return a + b;
}
class=class="tok-str">"tok-cmt">// public - access modifier (public, protected, private, or default)
class=class="tok-str">"tok-cmt">// static - optional modifier (static, final, synchronized, etc.)
class=class="tok-str">"tok-cmt">// int - return type
class=class="tok-str">"tok-cmt">// add - method name
class=class="tok-str">"tok-cmt">// (int a, int b) - parameter list (may be empty)
class=class="tok-str">"tok-cmt">// { ... } - method body
The signature of a method is its name plus the list of parameter types — it does not include the return type or the modifiers. Two methods in the same class with the same signature are illegal.
2. Static vs Instance Methods
A static method belongs to the class itself. You call it on the class name, no object needed. Math.max(a, b) and Integer.parseInt(s) are static methods. An instance method belongs to an object; you must have an instance to call it.
public class Calculator {
class=class="tok-str">"tok-cmt">// static - call as Calculator.add(class="tok-num">2, class="tok-num">3)
public static int add(int a, int b) {
return a + b;
}
class=class="tok-str">"tok-cmt">// instance - call on an object: calc.multiply(class="tok-num">2, class="tok-num">3)
private int factor;
public Calculator(int factor) {
this.factor = factor;
}
public int multiply(int n) {
return n * factor;
}
}
class=class="tok-str">"tok-cmt">// usage:
int sum = Calculator.add(class="tok-num">2, class="tok-num">3);
Calculator calc = new Calculator(class="tok-num">10);
int prod = calc.multiply(class="tok-num">5); class=class="tok-str">"tok-cmt">// class="tok-num">50
Static methods are appropriate for utility functions that do not depend on object state. The main entry point is always static because the JVM has no instance to call it on.
3. Parameters and Arguments
Java passes all arguments by value. For primitives this means the value is copied into the parameter. For references, the reference itself is copied — but both the original and the copy point to the same object, so the method can mutate that object's state.
class=class="tok-str">"tok-cmt">// primitive - the value is copied
public void doubleIt(int x) {
x = x * class="tok-num">2; class=class="tok-str">"tok-cmt">// local change, lost when method returns
}
int n = class="tok-num">10;
doubleIt(n);
class=class="tok-str">"tok-cmt">// n is still class="tok-num">10
class=class="tok-str">"tok-cmt">// reference - the reference is copied, both point to same object
public void appendBang(StringBuilder sb) {
sb.append("!");
}
StringBuilder sb = new StringBuilder("hi");
appendBang(sb);
class=class="tok-str">"tok-cmt">// sb.toString() is "hi!"
This is sometimes called “pass by value of the reference” or “pass by sharing”. The key point: a method cannot reassign the caller's variable, but it can mutate a shared object.
4. Return Values
A method returns at most one value, of the declared return type. void means no value is returned. Use return to exit early:
public int square(int n) {
return n * n;
}
public void printGreeting(String name) {
System.out.println("Hello, " + name);
class=class="tok-str">"tok-cmt">// no return statement needed - void method
}
public int findFirstEven(int[] nums) {
for (int n : nums) {
if (n % class="tok-num">2 == class="tok-num">0) return n; class=class="tok-str">"tok-cmt">// early exit
}
return -class="tok-num">1; class=class="tok-str">"tok-cmt">// not found
}
Since Java 17, you can also return multiple values cleanly with a record:
class=class="tok-str">"tok-cmt">// a record carries multiple named values
public record Range(int min, int max) {}
public Range findRange(int[] nums) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int n : nums) {
if (n < min) min = n;
if (n > max) max = n;
}
return new Range(min, max);
}
Range r = findRange(new int[]{class="tok-num">3, class="tok-num">1, class="tok-num">4, class="tok-num">1, class="tok-num">5, class="tok-num">9, class="tok-num">2, class="tok-num">6});
System.out.println(r.min() + " - " + r.max()); class=class="tok-str">"tok-cmt">// class="tok-num">1 - class="tok-num">9
5. Method Overloading
Overloading means having multiple methods with the same name but different parameter lists. The compiler picks the right one based on the argument types at the call site:
public class Printer {
public void print(int n) {
System.out.println("Integer: " + n);
}
public void print(String s) {
System.out.println("String: " + s);
}
public void print(double d) {
System.out.println("Double: " + d);
}
}
class=class="tok-str">"tok-cmt">// usage - the compiler picks the matching overload
Printer p = new Printer();
p.print(class="tok-num">42); class=class="tok-str">"tok-cmt">// prints "Integer: class="tok-num">42"
p.print("hello"); class=class="tok-str">"tok-cmt">// prints "String: hello"
p.print(class="tok-num">3.14); class=class="tok-str">"tok-cmt">// prints "Double: class="tok-num">3.14"
Use overloading sparingly. Two methods named find that take a String vs an int is fine; ten overloads that differ only in minor type variations becomes hard to read.
6. Variable Argument Lists (varargs)
Use ... after the type to accept any number of arguments. Inside the method, the parameter is an array:
public int sum(int... nums) {
int total = class="tok-num">0;
for (int n : nums) total += n;
return total;
}
class=class="tok-str">"tok-cmt">// call with any number of arguments
int a = sum(); class=class="tok-str">"tok-cmt">// class="tok-num">0
int b = sum(class="tok-num">1, class="tok-num">2); class=class="tok-str">"tok-cmt">// class="tok-num">3
int c = sum(class="tok-num">1, class="tok-num">2, class="tok-num">3, class="tok-num">4, class="tok-num">5); class=class="tok-str">"tok-cmt">// class="tok-num">15
int d = sum(new int[]{class="tok-num">1, class="tok-num">2, class="tok-num">3}); class=class="tok-str">"tok-cmt">// also works - explicit array
class=class="tok-str">"tok-cmt">// printf uses varargs for the format args
System.out.printf("%s is %d years old%n", "Alice", class="tok-num">30);
Varargs is syntactic sugar: the compiler builds an array at the call site. System.out.printf uses varargs for the format arguments.
7. Recursion
A method can call itself. Use recursion when the problem is naturally self-similar (trees, divide-and-conquer, factorial, Fibonacci) and avoid it when an iterative solution is just as clear, because each call adds a stack frame:
class=class="tok-str">"tok-cmt">// classic factorial
public int factorial(int n) {
if (n <= class="tok-num">1) return class="tok-num">1; class=class="tok-str">"tok-cmt">// base case
return n * factorial(n - class="tok-num">1); class=class="tok-str">"tok-cmt">// recursive case
}
class=class="tok-str">"tok-cmt">// factorial(class="tok-num">5) -> class="tok-num">5 * class="tok-num">4 * class="tok-num">3 * class="tok-num">2 * class="tok-num">1 = class="tok-num">120
class=class="tok-str">"tok-cmt">// binary search - divide and conquer
public int binarySearch(int[] arr, int target, int lo, int hi) {
if (lo > hi) return -class="tok-num">1;
int mid = (lo + hi) / class="tok-num">2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) return binarySearch(arr, target, mid + class="tok-num">1, hi);
return binarySearch(arr, target, lo, mid - class="tok-num">1);
}
Every recursive call adds a frame to the JVM call stack. Deep or unbounded recursion will throw StackOverflowError. Tail-call optimisation is not implemented in the JVM.
8. Naming and Design
Method names are verbs in lowerCamelCase: computeTotal, sendEmail, findById. A good method does one thing, fits on a screen, and has a name that tells you exactly what it does without you having to read the body. If you find yourself writing “and” in the name (computeTotalAndApplyDiscount), split it.
Methods should be short — ten to twenty lines is a healthy target. Long methods are hard to read, hard to test, and tend to mix concerns. The Extract Method refactor in any IDE turns a chunk of a long method into a well-named private helper in seconds.
Exercises
- Write a static method
int max(int a, int b)that returns the larger of two numbers. - Overload it as
int max(int a, int b, int c)for three numbers. - Write a varargs method
int sum(int... nums)that sums any number of integers. - Write a recursive method
int factorial(int n)and call it with values 0 through 10. - Refactor a 30-line
maininto three well-named private helper methods.