A method is a named block of code that performs a specific task. You define it once and then call (invoke) it as many times as you need. Methods are the fundamental building blocks of behavior in Java — they describe what an object can do.
Think of it in real-world terms: if a class represents a noun (a Car, a Student, a BankAccount), then its methods represent the verbs — the actions that noun can perform. A Car can start(), accelerate(), and brake(). A BankAccount can deposit(), withdraw(), and getBalance().
calculateTax() method instead of copying and pasting the formula ten times.Collections.sort(list) without knowing the sorting algorithm used internally.calculateDiscount(price, percentage) returns the correct value without running the entire application.Every Java program starts executing at a method: public static void main(String[] args). By the end of this tutorial, you will understand every keyword in that signature and be able to design well-structured methods of your own.
Every method in Java follows a specific structure. Here is the complete anatomy:
accessModifier [static] [final] returnType methodName(parameterList) [throws ExceptionType] {
// method body - the statements that execute when the method is called
return value; // required if returnType is not void
}
Let us break down each component with a concrete example:
public static double calculateTax(double price, double taxRate) {
double tax = price * taxRate;
return tax;
}
| Component | In Our Example | Purpose |
|---|---|---|
| Access Modifier | public |
Controls who can call this method (public, private, protected, or default) |
| Static Modifier | static |
Optional. Makes the method belong to the class itself rather than an instance |
| Return Type | double |
The data type of the value the method sends back to the caller |
| Method Name | calculateTax |
A descriptive name following camelCase convention |
| Parameter List | (double price, double taxRate) |
Input values the method needs to do its work. Can be empty: () |
| Method Body | { double tax = price * taxRate; return tax; } |
The code that executes when the method is called, enclosed in curly braces |
| Return Statement | return tax; |
Sends a value back to the caller. Omitted only if return type is void |
The combination of a method’s name and its parameter list is called the method signature. The method signature is what Java uses to distinguish one method from another. For example, calculateTax(double, double) is the signature of the method above. Note that the return type is not part of the signature.
Good method names make code self-documenting. Follow these conventions:
calculateTotal, not CalculateTotal or calculate_totalget, set, calculate, validate, is, has, find, create, deletecalculateMonthlyPayment() is better than calc()isEmpty(), hasPermission(), isValid()// Good method names - clear and descriptive
public double calculateMonthlyPayment(double principal, double rate, int months) { ... }
public boolean isEligibleForDiscount(Customer customer) { ... }
public List findOrdersByCustomerId(int customerId) { ... }
public void sendWelcomeEmail(String emailAddress) { ... }
// Bad method names - vague or poorly formatted
public double calc(double a, double b, int c) { ... } // too vague
public boolean check(Customer c) { ... } // check what?
public List data(int id) { ... } // noun, not a verb
public void DoSomething(String s) { ... } // wrong casing
Access modifiers control the visibility of a method — who is allowed to call it. Java provides four levels of access:
| Modifier | Same Class | Same Package | Subclass (different package) | Any Class |
|---|---|---|---|---|
public |
Yes | Yes | Yes | Yes |
protected |
Yes | Yes | Yes | No |
| default (no keyword) | Yes | Yes | No | No |
private |
Yes | No | No | No |
public — The method is part of the class’s public API. Other classes are expected to call it. Example: public double getBalance()private — The method is an internal implementation detail. It is a helper that only this class should call. Example: private boolean validateAccountNumber(String number)protected — The method is meant for use by subclasses (child classes) that extend this class, or by other classes in the same package. Example: protected void onInitialize()void processInternalQueue()Rule of thumb: Start with private and widen the access only when necessary. This is the principle of least privilege — exposing as little as possible keeps your code safe and easy to refactor.
public class BankAccount {
private double balance;
// PUBLIC - anyone can call this to deposit money
public void deposit(double amount) {
if (validateAmount(amount)) {
balance += amount;
logTransaction("DEPOSIT", amount);
}
}
// PUBLIC - anyone can check the balance
public double getBalance() {
return balance;
}
// PRIVATE - only this class needs to validate amounts
private boolean validateAmount(double amount) {
return amount > 0;
}
// PRIVATE - internal logging, not part of the public API
private void logTransaction(String type, double amount) {
System.out.println(type + ": $" + amount + " | Balance: $" + balance);
}
// PROTECTED - subclasses (e.g., SavingsAccount) may override interest calculation
protected double calculateInterest() {
return balance * 0.01;
}
// DEFAULT (package-private) - only classes in the same package can call this
void processEndOfDay() {
balance += calculateInterest();
}
}
The return type declares what kind of value a method sends back to the caller. It can be any data type: a primitive, an object, an array, a collection, or the special keyword void meaning “this method returns nothing.”
A void method performs an action but does not return a value. These are common for methods that print output, modify an object’s state, or trigger side effects.
public void greet(String name) {
System.out.println("Hello, " + name + "!");
// no return statement needed (though you can use 'return;' to exit early)
}
public void setAge(int age) {
if (age < 0) {
System.out.println("Age cannot be negative.");
return; // early exit - still valid in a void method
}
this.age = age;
}
public int add(int a, int b) {
return a + b;
}
public double calculateAverage(double[] scores) {
double sum = 0;
for (double score : scores) {
sum += score;
}
return sum / scores.length;
}
public boolean isPrime(int number) {
if (number <= 1) return false;
if (number <= 3) return true;
if (number % 2 == 0 || number % 3 == 0) return false;
for (int i = 5; i * i <= number; i += 6) {
if (number % i == 0 || number % (i + 2) == 0) return false;
}
return true;
}
public String getFullName(String firstName, String lastName) {
return firstName + " " + lastName;
}
public LocalDate parseDate(String dateString) {
return LocalDate.parse(dateString); // returns a LocalDate object
}
// Returning a custom object
public Student createStudent(String name, int age, double gpa) {
Student student = new Student();
student.setName(name);
student.setAge(age);
student.setGpa(gpa);
return student;
}
// Returning an array
public int[] getFirstNPrimes(int n) {
int[] primes = new int[n];
int count = 0;
int number = 2;
while (count < n) {
if (isPrime(number)) {
primes[count] = number;
count++;
}
number++;
}
return primes;
}
// Returning a List
public List filterLongWords(List words, int minLength) {
List result = new ArrayList<>();
for (String word : words) {
if (word.length() >= minLength) {
result.add(word);
}
}
return result;
}
// Returning a Map
public Map countWordFrequency(String text) {
Map frequency = new HashMap<>();
String[] words = text.toLowerCase().split("\\s+");
for (String word : words) {
frequency.put(word, frequency.getOrDefault(word, 0) + 1);
}
return frequency;
}
Important: Every code path through a non-void method must end with a return statement. The compiler will reject code where a path exists that does not return a value.
// COMPILE ERROR - missing return on the else path
public String getGrade(int score) {
if (score >= 90) {
return "A";
}
// What if score is less than 90? No return statement!
}
// CORRECT - all paths return a value
public String getGrade(int score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
if (score >= 60) return "D";
return "F"; // default covers all remaining cases
}
Parameters are the variables listed in a method's declaration. Arguments are the actual values you pass when you call the method. While often used interchangeably in casual conversation, the distinction matters:
// parameters: price and taxRate
// v v
public double calculateTax(double price, double taxRate) {
return price * taxRate;
}
// Calling the method:
// arguments: 99.99 and 0.08
// v v
double tax = calculateTax(99.99, 0.08);
This is one of the most misunderstood topics in Java. Java always passes arguments by value. There is no pass-by-reference in Java. But what "by value" means differs for primitives and objects:
For primitives: Java copies the actual value. Changing the parameter inside the method has zero effect on the original variable.
public class PassByValueDemo {
public static void tryToChange(int number) {
number = 999; // modifies the local copy only
System.out.println("Inside method: " + number);
}
public static void main(String[] args) {
int original = 42;
tryToChange(original);
System.out.println("After method call: " + original);
}
}
// Output:
// Inside method: 999
// After method call: 42 <-- original is unchanged!
For objects: Java copies the reference (the memory address), not the object itself. Both the original variable and the parameter point to the same object in memory. So you can modify the object's contents through the parameter. However, reassigning the parameter to a new object does not affect the original variable.
public class PassByValueObjects {
public static void modifyList(List list) {
list.add("Cherry"); // WORKS - modifies the same object the caller sees
System.out.println("Inside method (after add): " + list);
list = new ArrayList<>(); // reassigns the LOCAL copy of the reference
list.add("Dragonfruit");
System.out.println("Inside method (after reassign): " + list);
}
public static void main(String[] args) {
List fruits = new ArrayList<>(Arrays.asList("Apple", "Banana"));
modifyList(fruits);
System.out.println("After method call: " + fruits);
}
}
// Output:
// Inside method (after add): [Apple, Banana, Cherry]
// Inside method (after reassign): [Dragonfruit]
// After method call: [Apple, Banana, Cherry] <-- Cherry was added, but reassignment had no effect
The takeaway: the method receives a copy of the reference. Through that copy, it can reach into the object and change its state. But it cannot make the caller's variable point to a different object.
Sometimes you do not know in advance how many arguments will be passed. Java supports varargs (variable-length arguments) using the ... syntax. Internally, Java treats varargs as an array.
public class VarargsDemo {
// The 'numbers' parameter accepts zero or more int values
public static int sum(int... numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
// Varargs can be combined with regular parameters
// but the varargs parameter MUST be the last one
public static String formatMessage(String prefix, String... lines) {
StringBuilder sb = new StringBuilder(prefix).append(":\n");
for (String line : lines) {
sb.append(" - ").append(line).append("\n");
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(sum()); // 0 arguments -> 0
System.out.println(sum(5)); // 1 argument -> 5
System.out.println(sum(1, 2, 3, 4, 5)); // 5 arguments -> 15
// You can also pass an array directly
int[] values = {10, 20, 30};
System.out.println(sum(values)); // 60
System.out.println(formatMessage("Errors", "Null pointer", "Out of bounds"));
}
}
// Output:
// 0
// 5
// 15
// 60
// Errors:
// - Null pointer
// - Out of bounds
Varargs rules to remember:
Method overloading means defining multiple methods with the same name but different parameter lists in the same class. Java determines which version to call based on the number, types, and order of arguments at compile time.
You use method overloading to provide multiple ways to do the same logical operation with different inputs. The Java standard library uses this pattern extensively — for example, System.out.println() has overloaded versions for int, double, String, Object, and more.
Overloaded methods must differ in at least one of these ways:
Overloaded methods cannot differ only by:
public class AreaCalculator {
// Overloaded method: calculate area of a square
public static double calculateArea(double side) {
return side * side;
}
// Overloaded method: calculate area of a rectangle
public static double calculateArea(double length, double width) {
return length * width;
}
// Overloaded method: calculate area of a triangle
public static double calculateArea(double base, double height, boolean isTriangle) {
return 0.5 * base * height;
}
// Overloaded method: calculate area of a circle (different parameter type)
public static double calculateArea(int radius) {
return Math.PI * radius * radius;
}
public static void main(String[] args) {
System.out.println("Square (5): " + calculateArea(5.0));
System.out.println("Rectangle (4 x 6): " + calculateArea(4.0, 6.0));
System.out.println("Triangle (3, 8): " + calculateArea(3.0, 8.0, true));
System.out.println("Circle (radius 7): " + calculateArea(7));
}
}
// Output:
// Square (5): 25.0
// Rectangle (4 x 6): 24.0
// Triangle (3, 8): 12.0
// Circle (radius 7): 153.93804002589985
Consider this: if you had two methods with the same name and same parameters but different return types, how would the compiler know which one you meant?
// COMPILE ERROR - these two methods have the same signature
public int getResult(int x) {
return x * 2;
}
public double getResult(int x) { // Same name, same parameters
return x * 2.0;
}
// The compiler cannot tell which one you want when you write:
// getResult(5); <-- ambiguous!
public class NotificationService {
// Send to one recipient
public void sendNotification(String recipient, String message) {
System.out.println("Sending to " + recipient + ": " + message);
}
// Send to multiple recipients
public void sendNotification(List recipients, String message) {
for (String recipient : recipients) {
sendNotification(recipient, message); // reuses the single-recipient version
}
}
// Send with a priority level
public void sendNotification(String recipient, String message, int priority) {
String prefix = (priority >= 5) ? "[URGENT] " : "";
sendNotification(recipient, prefix + message);
}
// Send with a delay
public void sendNotification(String recipient, String message, long delayMillis) {
System.out.println("Scheduled in " + delayMillis + "ms to " + recipient + ": " + message);
}
}
This is a critical distinction that every Java developer must understand. A static method belongs to the class itself. An instance method belongs to a specific object (instance) of the class.
| Feature | Static Method | Instance Method |
|---|---|---|
| Belongs to | The class | An object (instance) |
| How to call | ClassName.method() |
object.method() |
| Can access instance variables? | No | Yes |
| Can access static variables? | Yes | Yes |
Can use this keyword? |
No | Yes |
| Common use | Utility/helper methods, factory methods | Operations on object state |
public class Temperature {
private double celsius; // instance variable - each Temperature object has its own
// INSTANCE METHOD - operates on this object's celsius value
public Temperature(double celsius) {
this.celsius = celsius;
}
public double toFahrenheit() {
return (this.celsius * 9.0 / 5.0) + 32;
}
public double toCelsius() {
return this.celsius;
}
// STATIC METHOD - does not need an object, just takes input and returns output
public static double convertCelsiusToFahrenheit(double celsius) {
return (celsius * 9.0 / 5.0) + 32;
}
public static double convertFahrenheitToCelsius(double fahrenheit) {
return (fahrenheit - 32) * 5.0 / 9.0;
}
public static void main(String[] args) {
// Using static methods - no object needed
double f = Temperature.convertCelsiusToFahrenheit(100);
System.out.println("100°C = " + f + "°F");
// Using instance methods - requires an object
Temperature boiling = new Temperature(100);
System.out.println("100°C = " + boiling.toFahrenheit() + "°F");
Temperature body = new Temperature(37);
System.out.println("Body temp: " + body.toFahrenheit() + "°F");
}
}
// Output:
// 100°C = 212.0°F
// 100°C = 212.0°F
// Body temp: 98.60000000000001°F
Static methods are the backbone of utility classes — classes that group related helper methods together. The Java standard library has many examples: Math, Collections, Arrays, Objects. These classes are never instantiated; you just call their static methods directly.
// Standard library utility methods - all static, no object needed int max = Math.max(10, 20); // 20 double sqrt = Math.sqrt(144); // 12.0 int abs = Math.abs(-42); // 42 Collections.sort(myList); Arrays.sort(myArray); String result = Objects.requireNonNull(input, "Input must not be null");
You can create your own utility class:
public final class StringUtils {
// Private constructor prevents instantiation
private StringUtils() {
throw new UnsupportedOperationException("Utility class cannot be instantiated");
}
public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}
public static boolean isNullOrBlank(String str) {
return str == null || str.isBlank();
}
public static String capitalize(String str) {
if (isNullOrEmpty(str)) return str;
return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
}
public static String truncate(String str, int maxLength) {
if (str == null || str.length() <= maxLength) return str;
return str.substring(0, maxLength - 3) + "...";
}
}
// Usage - called on the class, not on an object
String name = StringUtils.capitalize("john"); // "John"
boolean empty = StringUtils.isNullOrEmpty(""); // true
String preview = StringUtils.truncate("Hello World, this is a long string", 15); // "Hello World,..."
main Static?The main method is the entry point of a Java application. The JVM calls it before any objects exist. Since no object has been created yet, the method must be static so that the JVM can invoke it on the class itself: MyApp.main(args).
Method chaining is a technique where each method returns the object itself (this), allowing multiple method calls to be linked together in a single statement. This produces fluent, readable code.
You have already seen this pattern if you have used StringBuilder:
// Without method chaining - repetitive variable reference
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
sb.append("!");
String result = sb.toString();
// With method chaining - fluent and concise
String result = new StringBuilder()
.append("Hello")
.append(" ")
.append("World")
.append("!")
.toString();
The key is to return this from methods that modify the object. This is the foundation of the Builder pattern, one of the most common design patterns in Java.
public class HttpRequest {
private String method;
private String url;
private Map headers = new HashMap<>();
private String body;
private int timeoutSeconds;
// Each setter returns 'this' to enable chaining
public HttpRequest method(String method) {
this.method = method;
return this;
}
public HttpRequest url(String url) {
this.url = url;
return this;
}
public HttpRequest header(String key, String value) {
this.headers.put(key, value);
return this;
}
public HttpRequest body(String body) {
this.body = body;
return this;
}
public HttpRequest timeout(int seconds) {
this.timeoutSeconds = seconds;
return this;
}
public void send() {
System.out.println(method + " " + url);
headers.forEach((k, v) -> System.out.println(" " + k + ": " + v));
if (body != null) System.out.println(" Body: " + body);
System.out.println(" Timeout: " + timeoutSeconds + "s");
}
public static void main(String[] args) {
// Clean, readable method chaining
new HttpRequest()
.method("POST")
.url("https://api.example.com/users")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer abc123")
.body("{\"name\": \"John\", \"email\": \"john@example.com\"}")
.timeout(30)
.send();
}
}
// Output:
// POST https://api.example.com/users
// Content-Type: application/json
// Authorization: Bearer abc123
// Body: {"name": "John", "email": "john@example.com"}
// Timeout: 30s
Method chaining is used extensively in modern Java: Stream API, StringBuilder, Optional, testing libraries like AssertJ and Mockito, and HTTP client libraries. Mastering this pattern will make your code both more readable and more idiomatic.
Recursion is when a method calls itself to solve a problem by breaking it into smaller sub-problems. Every recursive method must have two parts:
StackOverflowError.The factorial of n (written as n!) is the product of all positive integers from 1 to n. For example, 5! = 5 × 4 × 3 × 2 × 1 = 120.
public class RecursionDemo {
// Recursive factorial
public static long factorial(int n) {
if (n < 0) throw new IllegalArgumentException("Negative numbers not allowed");
if (n <= 1) return 1; // BASE CASE: 0! = 1, 1! = 1
return n * factorial(n - 1); // RECURSIVE CASE: n! = n * (n-1)!
}
public static void main(String[] args) {
System.out.println("5! = " + factorial(5)); // 120
System.out.println("10! = " + factorial(10)); // 3628800
System.out.println("0! = " + factorial(0)); // 1
}
}
// How factorial(5) executes:
// factorial(5) = 5 * factorial(4)
// = 5 * 4 * factorial(3)
// = 5 * 4 * 3 * factorial(2)
// = 5 * 4 * 3 * 2 * factorial(1)
// = 5 * 4 * 3 * 2 * 1
// = 120
The Fibonacci sequence is: 0, 1, 1, 2, 3, 5, 8, 13, 21, ... where each number is the sum of the two preceding ones.
public class FibonacciDemo {
// Simple recursive Fibonacci - correct but SLOW for large n
// Time complexity: O(2^n) because it recalculates the same values many times
public static long fibRecursive(int n) {
if (n <= 0) return 0; // base case
if (n == 1) return 1; // base case
return fibRecursive(n - 1) + fibRecursive(n - 2); // recursive case
}
// Iterative Fibonacci - much faster: O(n) time, O(1) space
public static long fibIterative(int n) {
if (n <= 0) return 0;
if (n == 1) return 1;
long prev = 0, current = 1;
for (int i = 2; i <= n; i++) {
long next = prev + current;
prev = current;
current = next;
}
return current;
}
public static void main(String[] args) {
// Both produce the same result
for (int i = 0; i <= 10; i++) {
System.out.println("fib(" + i + ") = " + fibIterative(i));
}
// But try fibRecursive(45) vs fibIterative(45) - huge difference in speed
long start = System.currentTimeMillis();
System.out.println("fibIterative(45) = " + fibIterative(45));
System.out.println("Iterative time: " + (System.currentTimeMillis() - start) + "ms");
start = System.currentTimeMillis();
System.out.println("fibRecursive(45) = " + fibRecursive(45));
System.out.println("Recursive time: " + (System.currentTimeMillis() - start) + "ms");
}
}
// Output:
// fib(0) = 0
// fib(1) = 1
// fib(2) = 1
// fib(3) = 2
// fib(4) = 3
// fib(5) = 5
// fib(6) = 8
// fib(7) = 13
// fib(8) = 21
// fib(9) = 34
// fib(10) = 55
// fibIterative(45) = 1134903170
// Iterative time: 0ms
// fibRecursive(45) = 1134903170
// Recursive time: ~5000ms (varies by machine)
| Use Recursion When | Use Iteration When |
|---|---|
| The problem is naturally recursive (tree traversal, directory walking, divide-and-conquer algorithms) | A simple loop can solve it (summing numbers, iterating a list) |
| The recursive solution is significantly more readable | Performance and memory matter (recursion uses stack frames) |
| Depth is bounded and predictable | Input size could be very large (risk of StackOverflowError) |
Every method call adds a frame to the call stack. Recursive calls pile up frames. If the recursion goes too deep (typically a few thousand calls), the stack runs out of space and Java throws a StackOverflowError.
// DANGER: no base case - infinite recursion
public static void infinite() {
infinite(); // calls itself forever
}
// Calling infinite() will crash with:
// Exception in thread "main" java.lang.StackOverflowError
// DANGER: base case that is never reached
public static int badFactorial(int n) {
return n * badFactorial(n - 1); // never stops because there is no base case for n <= 1
}
Writing methods that work is the minimum bar. Writing methods that are clean, maintainable, and easy to understand is what separates a senior developer from a beginner. Here are the practices that matter most.
A method should do one thing and do it well. If you find yourself writing a method name with "and" in it (validateAndSave, fetchAndProcess), that is a sign it should be two methods.
// BAD - does too many things
public void processOrder(Order order) {
// validate
if (order.getItems().isEmpty()) throw new IllegalArgumentException("No items");
if (order.getCustomer() == null) throw new IllegalArgumentException("No customer");
// calculate total
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
order.setTotal(total);
// apply discount
if (order.getCustomer().isVip()) {
order.setTotal(total * 0.9);
}
// save to database
database.save(order);
// send confirmation email
emailService.send(order.getCustomer().getEmail(), "Order confirmed!");
}
// GOOD - each method has a single responsibility
public void processOrder(Order order) {
validateOrder(order);
calculateTotal(order);
applyDiscounts(order);
saveOrder(order);
sendConfirmation(order);
}
private void validateOrder(Order order) {
if (order.getItems().isEmpty()) throw new IllegalArgumentException("No items");
if (order.getCustomer() == null) throw new IllegalArgumentException("No customer");
}
private void calculateTotal(Order order) {
double total = order.getItems().stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
order.setTotal(total);
}
private void applyDiscounts(Order order) {
if (order.getCustomer().isVip()) {
order.setTotal(order.getTotal() * 0.9);
}
}
private void saveOrder(Order order) {
database.save(order);
}
private void sendConfirmation(Order order) {
emailService.send(order.getCustomer().getEmail(), "Order confirmed!");
}
A good method fits on one screen (roughly 20-30 lines). If a method is longer, look for opportunities to extract sub-steps into separate methods. Short methods are easier to read, test, and debug.
Ideally, a method should have 0 to 3 parameters. More than 4 is a code smell. If you need many parameters, consider grouping them into an object.
// BAD - too many parameters, hard to remember the order
public User createUser(String firstName, String lastName, String email,
String phone, String address, String city,
String state, String zipCode, boolean isActive) {
// ...
}
// GOOD - group related data into an object
public User createUser(UserRegistration registration) {
// ...
}
// The UserRegistration class groups the parameters logically
public class UserRegistration {
private String firstName;
private String lastName;
private String email;
private String phone;
private Address address;
private boolean active;
// constructor, getters, setters...
}
A method named getBalance() should return the balance, not also send an email or write to a log file. If a method does something beyond what its name suggests, it has a side effect. Side effects make code unpredictable and hard to debug.
Use guard clauses (early returns) to handle edge cases at the top of the method. This keeps the "happy path" at the lowest indentation level.
// BAD - deeply nested, hard to follow
public double calculateDiscount(Customer customer, Order order) {
double discount = 0;
if (customer != null) {
if (order != null) {
if (!order.getItems().isEmpty()) {
if (customer.isVip()) {
discount = order.getTotal() * 0.15;
} else if (order.getTotal() > 100) {
discount = order.getTotal() * 0.05;
}
}
}
}
return discount;
}
// GOOD - guard clauses flatten the logic
public double calculateDiscount(Customer customer, Order order) {
if (customer == null || order == null) return 0;
if (order.getItems().isEmpty()) return 0;
if (customer.isVip()) {
return order.getTotal() * 0.15;
}
if (order.getTotal() > 100) {
return order.getTotal() * 0.05;
}
return 0;
}
Use Javadoc comments for public methods, especially those with non-obvious behavior, edge cases, or multiple parameters.
/**
* Calculates the monthly payment for a fixed-rate loan.
*
* @param principal the loan amount in dollars (must be positive)
* @param annualRate the annual interest rate as a decimal (e.g., 0.05 for 5%)
* @param months the number of monthly payments (must be at least 1)
* @return the fixed monthly payment amount, rounded to 2 decimal places
* @throws IllegalArgumentException if principal or months is non-positive, or rate is negative
*/
public static double calculateMonthlyPayment(double principal, double annualRate, int months) {
if (principal <= 0) throw new IllegalArgumentException("Principal must be positive");
if (months < 1) throw new IllegalArgumentException("Months must be at least 1");
if (annualRate < 0) throw new IllegalArgumentException("Rate cannot be negative");
if (annualRate == 0) return principal / months;
double monthlyRate = annualRate / 12;
double payment = principal * (monthlyRate * Math.pow(1 + monthlyRate, months))
/ (Math.pow(1 + monthlyRate, months) - 1);
return Math.round(payment * 100.0) / 100.0;
}
Here are the mistakes that trip up Java developers most often when working with methods. Learning to recognize these patterns will save you hours of debugging.
// COMPILE ERROR - missing return on the 'else' path
public String getStatus(int code) {
if (code == 200) {
return "OK";
} else if (code == 404) {
return "Not Found";
}
// What if code is 500? The compiler sees a path with no return.
}
// FIX - always have a default return
public String getStatus(int code) {
if (code == 200) return "OK";
if (code == 404) return "Not Found";
if (code == 500) return "Internal Server Error";
return "Unknown Status: " + code; // default handles all other cases
}
// MISTAKE - trying to "swap" two variables through a method
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
// This only swaps the LOCAL copies. The caller's variables are unchanged.
}
int x = 10, y = 20;
swap(x, y);
System.out.println(x + ", " + y); // Still prints: 10, 20
// MISTAKE - trying to "reset" a String through a method
public static void reset(String str) {
str = ""; // reassigns the local reference, not the caller's variable
}
String name = "Alice";
reset(name);
System.out.println(name); // Still prints: Alice
public class StaticContextError {
private String name = "Java";
public void printName() {
System.out.println(name);
}
public static void main(String[] args) {
// COMPILE ERROR - cannot call instance method from static context
// printName();
// FIX - create an instance first
StaticContextError obj = new StaticContextError();
obj.printName(); // Now it works
}
}
// MISTAKE - the base case is wrong, never terminates for negative input
public static int countdown(int n) {
if (n == 0) return 0; // only stops at exactly 0
return countdown(n - 1); // what if n starts negative? It goes -1, -2, -3... forever
}
countdown(-1); // StackOverflowError!
// FIX - use <= instead of ==
public static int countdown(int n) {
if (n <= 0) return 0; // handles 0 AND negative numbers
return countdown(n - 1);
}
// MISTAKE - String methods return a NEW string; they do not modify the original String name = " Hello World "; name.trim(); // returns a new trimmed string, but we throw it away name.toUpperCase(); // same problem System.out.println(name); // " Hello World " - unchanged! // FIX - capture the return value String name = " Hello World "; name = name.trim(); name = name.toUpperCase(); System.out.println(name); // "HELLO WORLD"
public class OverloadingTrap {
public static void print(int value) {
System.out.println("int: " + value);
}
public static void print(long value) {
System.out.println("long: " + value);
}
public static void print(Integer value) {
System.out.println("Integer: " + value);
}
public static void main(String[] args) {
print(42); // Calls print(int) - exact match
print(42L); // Calls print(long) - exact match
print(Integer.valueOf(42)); // Calls print(Integer) - exact match
// What about just print(42)?
// Java prefers: exact match > widening > autoboxing > varargs
// So print(42) calls print(int), NOT print(Integer) or print(long)
}
}
Let us put everything together in a complete, runnable program that demonstrates method syntax, access modifiers, return types, parameters, overloading, static vs instance methods, and best practices. This StudentGradeManager processes student grades and generates a report.
import java.util.ArrayList;
import java.util.List;
public class StudentGradeManager {
// ===== Instance Variables =====
private String studentName;
private List grades;
// ===== Constructor =====
public StudentGradeManager(String studentName) {
this.studentName = studentName;
this.grades = new ArrayList<>();
}
// ===== Instance Methods =====
/**
* Adds a single grade to this student's record.
* @param grade a value between 0.0 and 100.0
*/
public void addGrade(double grade) {
if (!isValidGrade(grade)) {
System.out.println("Invalid grade: " + grade + " (must be 0-100)");
return; // early return - guard clause
}
grades.add(grade);
}
/**
* Adds multiple grades at once using varargs.
* Demonstrates method overloading and varargs.
*/
public void addGrade(double... newGrades) {
for (double grade : newGrades) {
addGrade(grade); // reuses the single-grade version for validation
}
}
/**
* Calculates and returns the average of all grades.
* @return the average, or 0.0 if no grades exist
*/
public double calculateAverage() {
if (grades.isEmpty()) return 0.0;
double sum = 0;
for (double grade : grades) {
sum += grade;
}
return sum / grades.size();
}
/**
* Returns the highest grade.
* @return the maximum grade, or 0.0 if no grades exist
*/
public double getHighestGrade() {
if (grades.isEmpty()) return 0.0;
double max = grades.get(0);
for (double grade : grades) {
if (grade > max) max = grade;
}
return max;
}
/**
* Returns the lowest grade.
* @return the minimum grade, or 0.0 if no grades exist
*/
public double getLowestGrade() {
if (grades.isEmpty()) return 0.0;
double min = grades.get(0);
for (double grade : grades) {
if (grade < min) min = grade;
}
return min;
}
/**
* Determines the letter grade for this student based on their average.
* Uses the static helper method internally.
*/
public String getLetterGrade() {
return convertToLetterGrade(calculateAverage());
}
/**
* Checks if the student is passing (average >= 60).
* Demonstrates a boolean-returning method with a descriptive name.
*/
public boolean isPassing() {
return calculateAverage() >= 60.0;
}
/**
* Returns the number of grades recorded.
*/
public int getGradeCount() {
return grades.size();
}
/**
* Generates a formatted report card for this student.
* This is the "orchestrator" method that calls other focused methods.
*/
public String generateReport() {
StringBuilder report = new StringBuilder();
report.append("========================================\n");
report.append(" STUDENT REPORT CARD\n");
report.append("========================================\n");
report.append(" Student: ").append(studentName).append("\n");
report.append(" Grades: ").append(grades).append("\n");
report.append(" Count: ").append(getGradeCount()).append("\n");
report.append(" Average: ").append(formatScore(calculateAverage())).append("\n");
report.append(" Highest: ").append(formatScore(getHighestGrade())).append("\n");
report.append(" Lowest: ").append(formatScore(getLowestGrade())).append("\n");
report.append(" Letter: ").append(getLetterGrade()).append("\n");
report.append(" Status: ").append(isPassing() ? "PASSING" : "FAILING").append("\n");
report.append("========================================\n");
return report.toString();
}
// ===== Private Helper Methods =====
/**
* Validates that a grade is within the acceptable range.
* Private because only this class needs this logic.
*/
private boolean isValidGrade(double grade) {
return grade >= 0.0 && grade <= 100.0;
}
/**
* Formats a score to one decimal place.
*/
private String formatScore(double score) {
return String.format("%.1f", score);
}
// ===== Static Utility Methods =====
/**
* Converts a numeric score to a letter grade.
* Static because it does not depend on any instance state.
*/
public static String convertToLetterGrade(double score) {
if (score >= 93) return "A";
if (score >= 90) return "A-";
if (score >= 87) return "B+";
if (score >= 83) return "B";
if (score >= 80) return "B-";
if (score >= 77) return "C+";
if (score >= 73) return "C";
if (score >= 70) return "C-";
if (score >= 67) return "D+";
if (score >= 63) return "D";
if (score >= 60) return "D-";
return "F";
}
/**
* Compares two students and returns the one with the higher average.
* Static because it operates on two objects, not "this" object.
*/
public static StudentGradeManager getTopStudent(StudentGradeManager s1, StudentGradeManager s2) {
return (s1.calculateAverage() >= s2.calculateAverage()) ? s1 : s2;
}
// Getter for name (used in main for display)
public String getStudentName() {
return studentName;
}
// ===== Main Method: Program Entry Point =====
public static void main(String[] args) {
// Create students and add grades
StudentGradeManager alice = new StudentGradeManager("Alice Johnson");
alice.addGrade(95.5, 88.0, 92.3, 78.0, 96.5); // varargs overload
StudentGradeManager bob = new StudentGradeManager("Bob Smith");
bob.addGrade(72.0); // single grade overload
bob.addGrade(65.5);
bob.addGrade(80.0);
bob.addGrade(55.0);
bob.addGrade(70.0);
StudentGradeManager charlie = new StudentGradeManager("Charlie Davis");
charlie.addGrade(45.0, 52.0, 38.0, 60.0, 55.5);
// Generate reports (instance method)
System.out.println(alice.generateReport());
System.out.println(bob.generateReport());
System.out.println(charlie.generateReport());
// Use static method directly on the class
System.out.println("Letter grade for 85.0: " + StudentGradeManager.convertToLetterGrade(85.0));
// Use static method to compare students
StudentGradeManager top = StudentGradeManager.getTopStudent(alice, bob);
System.out.println("Top student: " + top.getStudentName()
+ " (avg: " + String.format("%.1f", top.calculateAverage()) + ")");
// Test invalid grade (guard clause in action)
alice.addGrade(105.0); // prints error message
alice.addGrade(-5.0); // prints error message
}
}
// Output:
// ========================================
// STUDENT REPORT CARD
// ========================================
// Student: Alice Johnson
// Grades: [95.5, 88.0, 92.3, 78.0, 96.5]
// Count: 5
// Average: 90.1
// Highest: 96.5
// Lowest: 78.0
// Letter: A-
// Status: PASSING
// ========================================
//
// ========================================
// STUDENT REPORT CARD
// ========================================
// Student: Bob Smith
// Grades: [72.0, 65.5, 80.0, 55.0, 70.0]
// Count: 5
// Average: 68.5
// Highest: 80.0
// Lowest: 55.0
// Letter: D+
// Status: PASSING
// ========================================
//
// ========================================
// STUDENT REPORT CARD
// ========================================
// Student: Charlie Davis
// Grades: [45.0, 52.0, 38.0, 60.0, 55.5]
// Count: 5
// Average: 50.1
// Highest: 60.0
// Lowest: 38.0
// Letter: F
// Status: FAILING
// ========================================
//
// Letter grade for 85.0: B
// Top student: Alice Johnson (avg: 90.1)
// Invalid grade: 105.0 (must be 0-100)
// Invalid grade: -5.0 (must be 0-100)
| Concept | Key Rule |
|---|---|
| Method Signature | Name + parameter types. Return type is NOT part of the signature. |
| Access Modifiers | private → default → protected → public (narrowest to widest) |
| void Methods | Perform an action, return nothing. Can use return; to exit early. |
| Pass-by-Value | Java always copies the value. For objects, it copies the reference (so you can modify the object but not reassign the caller's variable). |
| Varargs | Use Type... name. Must be the last parameter. Treated as an array inside the method. |
| Overloading | Same name, different parameter list. Return type alone is not enough. |
| Static Methods | Belong to the class. Cannot use this or access instance variables. |
| Instance Methods | Belong to an object. Can access everything: instance vars, static vars, this. |
| Method Chaining | Return this from setter-like methods to enable fluent API style. |
| Recursion | Always define a base case. Prefer iteration for simple loops. |
| Best Practice | Single responsibility. Max 3-4 parameters. Start private, widen as needed. Return early. |