A method is a named block of code that takes inputs and optionally produces a result. Methods are how you stop repeating yourself and how you break a large problem into pieces small enough to understand one at a time.
Anatomy
public int add(int a, int b) {
return a + b;
}
public— who can call it. Packages covers the four options.int— the return type. The type of the value it hands back.add— the name. A verb, incamelCase.(int a, int b)— the parameters, the inputs it needs.return a + b;— produces the result and exits immediately.
A method that does something rather than computing something has the return type
void, and either omits return or uses a bare return; to exit
early:
public void printGreeting(String name) {
if (name == null) {
return; // leave now, produce nothing
}
System.out.println("Hello, " + name);
}
The compiler will not let a non-void method finish without returning something on
every path, which catches a real class of mistake:
public String describe(int score) {
if (score > 50) {
return "pass";
}
return "fail"; // remove this line and it will not compile
}
Calling a method
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public static int square(int n) { // static — no object needed
return n * n;
}
}
class Demo {
void run() {
Calculator calc = new Calculator();
int sum = calc.add(2, 3); // instance method: needs an object
int sq = Calculator.square(4); // static method: called on the class
System.out.println(sum + " " + sq); // 5 16
}
}
The distinction is about whether the method needs the object's data. square works
entirely from its argument, so it does not need an instance and should be static. A
method that reads or changes fields must be an instance method.
Pass by value — including for objects
This is the part of methods that genuinely confuses people, and the confusion comes from a half-true summary. Java is always pass-by-value. What gets copied for an object is the reference, not the object.
void changeNumber(int n) {
n = 99; // changes the local copy only
}
void changeArray(int[] arr) {
arr[0] = 99; // follows the reference — the caller sees this
}
void replaceArray(int[] arr) {
arr = new int[]{99}; // repoints the local copy — the caller sees nothing
}
void demo() {
int number = 1;
changeNumber(number);
System.out.println(number); // 1 — unchanged
int[] data = {1, 2, 3};
changeArray(data);
System.out.println(data[0]); // 99 — the array itself was modified
int[] other = {1, 2, 3};
replaceArray(other);
System.out.println(other[0]); // 1 — still the original array
}
Compare the last two. changeArray modifies the object the reference points at, which
the caller shares. replaceArray assigns a new reference to its own parameter variable,
which the caller never sees. Both are "pass by value" — the value copied is the reference.
The practical consequence: a method that takes a mutable object can change the caller's data, and
you should say so in the name. sortInPlace(list) is honest; process(list)
that quietly reorders it is not.
Overloading
Several methods can share a name if their parameter lists differ:
public class Printer {
void print(String s) { System.out.println("text: " + s); }
void print(int n) { System.out.println("number: " + n); }
void print(int a, int b) { System.out.println("two: " + a + "," + b); }
}
The compiler picks the overload by the argument types, at compile time. The return type is not part of the signature — two methods differing only by return type will not compile.
Overloading is useful for genuine convenience variants. It becomes a trap when the overloads do different things, or when the compiler's choice is not the obvious one:
List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.remove(1); // remove(int index) -> removes 20
System.out.println(list); // [10, 30]
list.remove(Integer.valueOf(30)); // remove(Object) -> removes the VALUE 30
System.out.println(list); // [10]
That is a real trap in the standard library, and it is worth seeing once so it does not cost you an afternoon.
Varargs
int sum(int... numbers) { // zero or more ints
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
void demo() {
System.out.println(sum()); // 0
System.out.println(sum(1, 2, 3)); // 6
System.out.println(sum(new int[]{1, 2})); // 3 — an array works too
}
Inside the method, numbers is an ordinary array. There can be only one varargs
parameter and it must be last.
A method that calls itself
Recursion is legal and occasionally the clearest expression of a problem that is defined in terms of itself. It needs two things: a base case that returns without recursing, and a step that moves toward it.
long factorial(int n) {
if (n <= 1) return 1; // base case — without this it never stops
return n * factorial(n - 1); // each call is closer to the base case
}
void demo() {
System.out.println(factorial(5)); // 120
}
Every call occupies a frame on the call stack, so a recursion that goes too deep — or one whose
base case is never reached — ends in StackOverflowError. For anything that might run
thousands deep, write a loop. Recursion earns its place on genuinely nested data such as trees and
directory listings, not on counting.
Writing methods worth reading
- One job each. If the name needs an "and", it is two methods.
- Name it after what it produces, not how.
calculateTotal, notdoTheMath. A boolean method reads as a question:isValid,hasExpired. - Few parameters. Past three, the call site becomes a puzzle of positional arguments. Group them into an object instead.
- Keep it short. There is no magic number, but a method you have to scroll is usually several methods that have not been separated yet.
- Return early. Guard clauses at the top, the real work unindented below.
Next
Methods and fields belong to classes, which you have been writing without explanation. Classes is next.