What is a Method?
A method is a named, reusable block of code that performs a specific task. You define it once and call it as many times as needed β from anywhere that has access to it. Without methods, every program would be a single unbroken sequence of instructions: impossible to read, impossible to test, impossible to maintain.
In Java, methods always live inside a class β there are no standalone functions like in Python or JavaScript. Every method has a clear contract: what it receives (parameters), what it does (body), and what it gives back (return type). The compiler enforces that contract at every call site.
// Without methods: repeated logic, no structure
double total1 = 99.99 * 1.21; // apply 21% VAT
double total2 = 49.50 * 1.21; // same logic repeated
double total3 = 15.00 * 1.21; // change the rate? update 3 places. miss one? bug.
// With a method: defined once, called everywhere, change in one place
public double applyVat(double price) {
return price * 1.21;
}
double total1 = applyVat(99.99);
double total2 = applyVat(49.50);
double total3 = applyVat(15.00);
Methods are also the unit of testing. You can write a test that
calls applyVat(100.0) and verifies the result is 121.0
β independently of the rest of the program. That's only possible because the logic
is isolated in a method with a clear input and output.
In Java, the terms are often used interchangeably, but technically: a function is a standalone block of code. A method is a function attached to a class or object. Since Java 8, lambdas let you treat methods as values and pass them around β but they still live inside a class. Every method in Java is either an instance method (belongs to an object) or a static method (belongs to the class itself).
Method Anatomy
In Java, methods live inside classes β no standalone functions. Every method has a signature (name + parameter types), a return type, and a body. The signature is what uniquely identifies a method for overloading and overriding.
/*
* access modifiers return name parameters throws
* β β β β β β
* βΌ βΌ βΌ βΌ βΌ βΌ
*/
public static String formatCurrency(double amount, String symbol)
throws IllegalArgumentException {
if (amount < 0) throw new IllegalArgumentException("Negative amount");
return String.format("%s%.2f", symbol, amount);
}
Access modifiers
| Modifier | Same class | Same package | Subclass | Everywhere |
|---|---|---|---|---|
public |
β | β | β | β |
protected |
β | β | β | β |
| (package-private) | β | β | β | β |
private |
β | β | β | β |
Start private. Widen only when something external actually needs it.
Most helper/internal methods should be private. This is not just style β
it reduces the public surface of your API and makes refactoring safer.
How Method Calls Work: The Call Stack
Every method call pushes a new stack frame onto the thread's call stack. The frame holds local variables, parameters, and the return address. When the method returns, the frame is popped and memory is reclaimed immediately.
/*
* CALL STACK β calculateTotal(100.0, 5) example
* ================================================
*
* βββββββββββββββββββββββββββββββββββββββββββββββββββββ
* β calculateTotal(double price, int quantity) β β active frame
* β price = 100.0 (argument copied) β
* β quantity = 5 (argument copied) β
* β subtotal = 500.0 (local variable) β
* β β return 500.0 β
* βββββββββββββββββββββββββββββββββββββββββββββββββββββ€
* β main() β β waiting
* β double result = calculateTotal(100.0, 5); β
* βββββββββββββββββββββββββββββββββββββββββββββββββββββ
*
* After return: calculateTotal frame is gone.
* result = 500.0 in main's frame.
*
* StackOverflowError = stack ran out of space
* (infinite recursion is the usual cause)
*/
Pass by value β always
Java is always pass-by-value. What gets copied depends on the type:
// PRIMITIVE: copy of the value β caller is unaffected
public void tryToChange(int x) {
x = 999; // local copy only
}
int n = 10;
tryToChange(n);
System.out.println(n); // Still 10
// OBJECT: copy of the REFERENCE β mutations to the object ARE visible
public void addItem(List<String> list) {
list.add("new"); // β modifies the shared object β
list = new ArrayList<>(); // β only changes local reference β caller unaffected
}
List<String> items = new ArrayList<>();
addItem(items);
System.out.println(items); // [new] β mutation visible, reassignment not
Parameters: varargs and final
Varargs β variable number of arguments
// int... is treated as int[] internally
public int sum(int... numbers) {
int total = 0;
for (int n : numbers) total += n;
return total;
}
sum(); // 0 β zero args allowed
sum(1, 2, 3); // 6
sum(new int[]{10, 20}); // 30 β can pass array directly
// Varargs MUST be last β mixing with other params
public String format(String template, Object... args) {
return String.format(template, args);
}
- One varargs parameter per method maximum
- Must be the last parameter in the list
- Avoid overloading methods where one uses varargs β resolution can be ambiguous
Effectively final parameters
// Parameters used in lambdas or anonymous classes must be effectively final
public void schedule(String message, int delayMs) {
// message and delayMs cannot be reassigned after this point
Executors.newSingleThreadScheduledExecutor()
.schedule(() -> System.out.println(message), delayMs, TimeUnit.MILLISECONDS);
}
// β This breaks it:
public void broken(String message) {
message = message.trim(); // reassignment β now NOT effectively final
Runnable r = () -> System.out.println(message); // ERROR
}
// β
Fix: use a new local variable
public void fixed(String message) {
final var trimmed = message.trim();
Runnable r = () -> System.out.println(trimmed);
}
Method Overloading
Same name, different parameter list. The compiler resolves which overload to call at compile time based on argument types β this is static dispatch, not polymorphism.
// Compiler picks the MOST SPECIFIC match
public void process(Object obj) { System.out.println("Object"); }
public void process(String str) { System.out.println("String"); }
public void process(int n) { System.out.println("int"); }
public void process(long n) { System.out.println("long"); }
process("hello"); // "String" β String more specific than Object
process(10); // "int" β exact match
process(10L); // "long" β exact match
short s = 5;
process(s); // "int" β short widens to int
process(Integer.valueOf(10)); // "Object" β Integer autoboxed, then β Object
Return type alone, access modifier, parameter names, and throws clause do NOT count.
int foo() and String foo() cannot coexist β the compiler
can't distinguish them at the call site when the return value is ignored.
Static vs Instance Methods
public class Counter {
private int count = 0; // instance state
private static int totalCreated = 0; // class-level state
public Counter() { totalCreated++; }
// INSTANCE β reads/writes this object's state
public void increment() { count++; }
public int getCount() { return count; }
// STATIC β no access to instance state, no 'this'
public static int getTotalCreated() { return totalCreated; }
public static int add(int a, int b) { return a + b; }
}
// Usage
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment(); c1.increment();
c1.getCount(); // 2 β this counter only
c2.getCount(); // 0 β different object
Counter.getTotalCreated(); // 2 β shared across all instances
| Use static when⦠| Use instance when⦠|
|---|---|
| No object state needed | Method reads/writes object fields |
| Utility / helper (Math, Collections) | Behaviour varies per object |
| Factory method | Method needs polymorphism / overriding |
Entry point (main) |
Implements an interface method |
Method Design Best Practices
// β BAD: one method doing too many things
public void processUserData(String data) {
// validates, parses, saves, emails, logs, updates stats...
}
// β
GOOD: Single Responsibility β compose focused methods
public void registerUser(String data) {
if (!validateUserData(data)) return;
User user = parseUserData(data);
saveUser(user);
notifyUser(user);
}
private boolean validateUserData(String data) { ... }
private User parseUserData(String data) { ... }
private void saveUser(User user) { ... }
private void notifyUser(User user) { ... }
// β BAD: too many parameters (hard to read, hard to test)
public Order createOrder(String product, int qty, double price,
String customer, String address, boolean express) { ... }
// β
GOOD: group into an object (record in Java 16+)
record OrderRequest(String product, int qty, double price,
String customer, String address, boolean express) {}
public Order createOrder(OrderRequest req) { ... }
// β
GOOD: or Builder pattern for optional parameters
Order order = Order.builder()
.product("Widget").quantity(5).customer(customer).express(true)
.build();
- Under 20 lines β if it's longer, it's probably doing too much
- Max 3-4 parameters β above that, use a parameter object
- Return early β validate preconditions at the top, return/throw fast
- Avoid null returns β return empty collections, Optional, or throw
- Name as verb + noun β
findUserById, notget - Booleans: ask a question β
isValid(),hasPermission()
Senior Topics: Method Design Patterns
Return Optional instead of null (Java 8+)
// β Forces callers to remember null checks β NPE waiting to happen
public User findById(long id) {
return db.find(id); // null if not found
}
// β
Contract is explicit: caller knows this may be absent
public Optional<User> findById(long id) {
return Optional.ofNullable(db.find(id));
}
// Caller is forced to handle the absence
findById(42L)
.map(User::getEmail)
.ifPresent(emailService::send);
Method references β methods as values (Java 8+)
// Lambdas calling a single method can be replaced with method references
List<String> names = List.of("Alice", "Bob", "Charlie");
// Lambda
names.forEach(name -> System.out.println(name));
// Method reference β cleaner when intent is obvious
names.forEach(System.out::println);
// Static method reference
names.stream().map(String::toUpperCase).toList();
// Instance method reference on a specific object
names.stream().filter(emailValidator::isValid).toList();
// Constructor reference
names.stream().map(User::new).toList();
Default methods in interfaces (Java 8+)
// Default methods add behaviour to interfaces without breaking existing implementations
public interface Notifiable {
void sendNotification(String message); // abstract β must implement
default void sendUrgent(String message) { // default β can override
sendNotification("[URGENT] " + message);
}
}
// Common real-world use: Comparator.comparing()
List<User> sorted = users.stream()
.sorted(Comparator.comparing(User::lastName)
.thenComparing(User::firstName))
.toList();
Covariant return types
// Overriding method can return a MORE SPECIFIC type (Java 5+)
public class Animal {
public Animal create() { return new Animal(); }
}
public class Dog extends Animal {
@Override
public Dog create() { return new Dog(); } // β
Dog is a subtype of Animal
}
// This is used extensively in the Builder pattern to return 'this' as the concrete type
Interview Questions
Q: What is the difference between method overloading and overriding?
Overloading: same name, different parameter list, resolved at compile time (static
dispatch). Overriding: subclass redefines a superclass method with the same signature,
resolved at runtime (dynamic dispatch via vtable). Overloading has nothing to do
with inheritance.
Q: Can you overload a method by changing only the return type?
No. Return type alone doesn't differentiate overloads β the compiler can't tell which
version to call when the return value is ignored: foo();.
Q: Can a method return multiple values?
Not directly. Options: return an array or collection; return a custom class; use a record
(Java 16+): record Result(int code, String message) {}. Records are the
cleanest solution for multiple return values.
Q: What does void mean?
The method produces no return value. You cannot assign a void method call to a variable.
void is a type keyword, not an object β unlike Void (capital V),
which is a wrapper class used in generics like Callable<Void>.
Q: How does method dispatch work for overridden vs overloaded methods?
Overloaded methods use static dispatch β the compiler picks the overload based on the
declared (static) type of the argument at compile time. Overridden methods use dynamic
dispatch β the JVM resolves the actual method at runtime based on the object's runtime
type. This is the core of polymorphism. Consequence: if you pass a Dog
declared as Animal to an overloaded method, the Animal overload
is picked, not Dog.
Q: When would you use a static factory method instead of a constructor?
Static factories have named semantics (Optional.of() vs
Optional.empty()),
can return subtypes or cached instances, and can return null or Optional. Constructors
always create new instances and their names are fixed. Effective Java (Bloch) recommends
static factories as the default for public APIs β they give you more flexibility to change
the implementation without breaking the interface.
Q: What is the difference between default methods and
abstract methods in interfaces?
Abstract methods define a contract β implementing classes must provide an implementation.
Default methods provide an implementation in the interface itself β implementing classes
inherit it but can override it. Default methods were added in Java 8 specifically to
evolve existing interfaces (like Iterable, Collection) without
breaking every class that already implemented them.
Q: What is an effectively final variable and why does it matter for lambdas?
A variable is effectively final if it's never reassigned after initialisation. Lambdas
and anonymous classes can only capture effectively final local variables because the
lambda may outlive the stack frame where the variable was declared β the JVM needs to
copy the value into the lambda's closure. If the variable could change, the copy would
be stale. This is why reassigning a variable and then using it in a lambda is a
compile error.