Methods in Java
Learn how Java methods work — parameters, return types, method overloading, scope, recursion, and the difference between pass-by-value and pass-by-reference.
A method is a named block of code that performs a specific task. Methods let you break programs into reusable pieces — define the logic once, call it from anywhere. Without methods, every program would be an unstructured wall of code that you’d have to duplicate every time you needed the same logic.
Method Anatomy
Understanding each part of a method signature is essential because Java uses every element — the access modifier, return type, name, and parameter types — to identify and resolve method calls.
// access modifier return type name parameters
public static int add (int a, int b) {
return a + b; // return statement
}
- Access modifier — who can call this method (
public,private,protected, or none) - Return type — the type of value the method gives back (
voidif it returns nothing) - Method name — camelCase by convention
- Parameters — typed input values the caller provides
- Return statement — required for non-void methods
A Complete Example
This example shows the most common method shapes in one class — no return value, returning primitives, returning booleans, and returning strings. These patterns cover the vast majority of methods you will write.
public class MathUtils {
// No input, no output — just runs a side effect
public static void printSeparator() {
System.out.println("-------------------");
}
// Takes two ints, returns their sum
public static int add(int a, int b) {
return a + b;
}
// Takes a double, returns a double
public static double celsiusToFahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32;
}
// Returns a boolean — names starting with "is" signal a yes/no question
public static boolean isEven(int n) {
return n % 2 == 0;
}
// Returns a String — builds the result before returning it
public static String repeat(String s, int times) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < times; i++) sb.append(s);
return sb.toString();
}
public static void main(String[] args) {
System.out.println(add(3, 4)); // 7
System.out.println(celsiusToFahrenheit(100)); // 212.0
System.out.println(isEven(7)); // false
System.out.println(repeat("ab", 3)); // ababab
printSeparator(); // -------------------
}
}
Parameters and Arguments
Parameters are the variables declared in the method signature. Arguments are the actual values passed when calling the method. The distinction matters when reading error messages — the compiler reports “wrong number of arguments” when your call site doesn’t match the parameter list.
// "base" and "exponent" are parameters
public static long power(int base, int exponent) {
long result = 1;
for (int i = 0; i < exponent; i++) result *= base;
return result;
}
// 2 and 10 are arguments
System.out.println(power(2, 10)); // 1024
Default Parameters — Java Has None
Java does not have default parameter values like Python or Kotlin. The standard workaround is overloading — write a shorter version that delegates to the full version with sensible defaults. This keeps validation logic in one place.
public static String greet(String name, String greeting) {
return greeting + ", " + name + "!";
}
// Overloaded version — provides a default greeting
public static String greet(String name) {
return greet(name, "Hello"); // delegate to full version
}
System.out.println(greet("Alice")); // Hello, Alice!
System.out.println(greet("Bob", "Good day")); // Good day, Bob!
Return Types
void — No Return Value
Methods declared void perform actions (printing, modifying state, sending a request) rather than producing a value. They are complete without a return statement.
public static void logError(String message) {
System.err.println("[ERROR] " + message);
// no return statement needed
}
Returning Early
A method can return before reaching the end — this is called a guard clause and is a powerful technique for handling edge cases at the top of a method, keeping the main logic unindented and easy to follow.
public static double divide(double a, double b) {
if (b == 0) {
System.out.println("Cannot divide by zero.");
return Double.NaN; // early return — skip the rest
}
return a / b;
}
Returning Multiple Values via a Record
Java methods can only return one value, but Java 16+ records give you a clean, typed container for bundling multiple return values without the overhead of writing a full class.
// Java 16+ records are perfect for returning multiple values
public record MinMax(int min, int max) {}
public static MinMax findMinMax(int[] arr) {
int min = arr[0], max = arr[0];
for (int val : arr) {
if (val < min) min = val;
if (val > max) max = val;
}
return new MinMax(min, max);
}
MinMax result = findMinMax(new int[]{5, 2, 8, 1, 9, 3});
System.out.println("Min: " + result.min()); // 1
System.out.println("Max: " + result.max()); // 9
Pass-by-Value vs Pass-by-Reference
Java is always pass-by-value, but the meaning differs for primitives vs objects. This is one of the most important concepts to understand correctly — misunderstanding it leads to bugs where you expect a method to change a variable but it doesn’t, or vice versa.
Primitives — A Copy is Passed
When you pass a primitive, the method gets its own copy. Changes to that copy have no effect on the caller’s variable.
public static void tryToDoubleIt(int x) {
x = x * 2; // only affects the local copy — caller is unchanged
}
int value = 5;
tryToDoubleIt(value);
System.out.println(value); // still 5 — the original was not changed
Objects — A Copy of the Reference is Passed
When you pass an object, the method gets a copy of the reference (the memory address). Both the caller and the method point to the same object, so mutations to the object’s contents are visible to the caller. But reassigning the parameter to a new object only changes the local copy of the reference.
public static void addItem(List<String> list, String item) {
list.add(item); // modifies the actual object via the reference copy
}
public static void replaceList(List<String> list) {
list = new ArrayList<>(); // only changes the local copy of the reference
list.add("new item");
}
List<String> names = new ArrayList<>(List.of("Alice", "Bob"));
addItem(names, "Charlie");
System.out.println(names); // [Alice, Bob, Charlie] — mutated
replaceList(names);
System.out.println(names); // [Alice, Bob, Charlie] — unchanged — replaceList had no effect
Variable Scope
A variable exists only within the block {} where it was declared. This scoping rule prevents name collisions and keeps variables tightly bound to the code that uses them, making programs easier to reason about.
public static void scopeDemo() {
int outer = 10; // accessible throughout the method
if (outer > 5) {
int inner = 20; // only accessible inside this if block
System.out.println(outer + inner); // 30
}
// System.out.println(inner); // COMPILE ERROR — inner is out of scope
for (int i = 0; i < 3; i++) {
int loopVar = i * 2; // new loopVar each iteration
System.out.println(loopVar);
}
// System.out.println(i); // COMPILE ERROR — i is out of scope
// System.out.println(loopVar); // COMPILE ERROR — loopVar is out of scope
}
Varargs — Variable Number of Arguments
A varargs parameter accepts zero or more values of the given type and exposes them as an array inside the method. It lets callers pass a natural comma-separated list rather than building an array explicitly. The varargs parameter must always be last in the signature.
public static int sum(int... numbers) {
int total = 0;
for (int n : numbers) total += n;
return total;
}
System.out.println(sum()); // 0
System.out.println(sum(1, 2, 3)); // 6
System.out.println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); // 55
// Can also pass an array directly
int[] values = {10, 20, 30};
System.out.println(sum(values)); // 60
Recursion
A recursive method calls itself. It solves a problem by breaking it into smaller versions of the same problem until it reaches a trivially simple case (the base case). Recursion is especially elegant for tree traversal, divide-and-conquer algorithms, and mathematical sequences.
// Factorial: n! = n × (n-1) × ... × 1
public static long factorial(int n) {
if (n <= 1) return 1; // base case — stops the recursion
return n * factorial(n - 1); // recursive case — problem gets smaller each call
}
// Fibonacci
public static int fibonacci(int n) {
if (n <= 1) return n; // base cases: fib(0)=0, fib(1)=1
return fibonacci(n - 1) + fibonacci(n - 2); // recursive case
}
System.out.println(factorial(10)); // 3628800
System.out.println(fibonacci(10)); // 55
For large inputs, prefer iterative solutions or memoisation — naive recursion re-computes the same values repeatedly and can overflow the stack.
Static vs Instance Methods
Static methods belong to the class itself — call them on the class name, no object needed. Instance methods belong to an object and can access that object’s fields. The rule of thumb: if the method needs no object state, make it static.
public class Counter {
private int count = 0; // instance field — belongs to each object
// Instance method — operates on this object's state
public void increment() { count++; }
public void decrement() { count--; }
public int getCount() { return count; }
// Static utility — no object state needed, pure calculation
public static int add(int a, int b){ return a + b; }
}
// Static — call on the class, no instance needed
System.out.println(Counter.add(3, 4)); // 7
// Instance — call on an object
Counter c = new Counter();
c.increment();
c.increment();
c.increment();
System.out.println(c.getCount()); // 3
Method Chaining
Methods that return this (or a new object of the same type) can be chained — the result of one call becomes the target of the next. This produces readable, fluent APIs where a sequence of configuration steps reads like a sentence.
public class QueryBuilder {
private String table = "";
private String condition = "";
private int limitVal = -1;
// Each method returns 'this' to enable chaining
public QueryBuilder from(String table) {
this.table = table;
return this;
}
public QueryBuilder where(String condition) {
this.condition = condition;
return this;
}
public QueryBuilder limit(int n) {
this.limitVal = n;
return this;
}
public String build() {
String sql = "SELECT * FROM " + table;
if (!condition.isEmpty()) sql += " WHERE " + condition;
if (limitVal > 0) sql += " LIMIT " + limitVal;
return sql;
}
}
String query = new QueryBuilder()
.from("users")
.where("age > 18")
.limit(10)
.build();
System.out.println(query);
// SELECT * FROM users WHERE age > 18 LIMIT 10