Skip to main content
Java · lesson 10 of 66
Open the Java page →
Java beginner Lesson 10 of 66

Methods in Java

Learn Java methods by running them: parameters, return values, records for multiple results, pass-by-value, scope, varargs and recursion.

By SkillByExample · Updated

A method is a named, reusable block of code: it takes parameters, runs, and optionally returns a value. Every example is a single file run with the JDK 21 source launcher (java File.java).

Anatomy of a method

public class Anatomy {
    //  modifiers  return  name    parameters
    public static  double  average(int a, int b) {
        double result = (a + b) / 2.0;
        return result;
    }

    public static void main(String[] args) {
        double avg = average(3, 8);
        System.out.println("average = " + avg);
        System.out.println(average(10, 10));
    }
}
$ java Anatomy.java
average = 5.5
10.0

The call average(3, 8) copies 3 and 8 into the parameters a and b, runs the body, and the return value replaces the call expression. Dividing by 2.0 rather than 2 forces floating-point division — (3 + 8) / 2 would give 5.

void methods and returning early

A void method returns nothing; a bare return still exits it early. Methods with a return type can return from several places.

public class Early {
    static void printPositive(int n) {
        if (n <= 0) {
            System.out.println("skipped " + n);
            return;                      // leave now
        }
        System.out.println("value " + n);
    }

    static String grade(int score) {
        if (score >= 90) return "A";
        if (score >= 80) return "B";
        return "C";
    }

    public static void main(String[] args) {
        printPositive(5);
        printPositive(-2);
        System.out.println(grade(95) + grade(85) + grade(40));
    }
}
$ java Early.java
value 5
skipped -2
ABC

The compiler checks that every path of a non-void method ends in a return (or a throw). Delete the last return "C"; and grade stops compiling with “missing return statement”.

Returning several values with a record

A method has one return type, so bundle related results in a record.

public class MinMaxDemo {
    record MinMax(int min, int max) {}

    static MinMax minMax(int[] values) {
        int min = values[0], max = values[0];
        for (int v : values) {
            if (v < min) min = v;
            if (v > max) max = v;
        }
        return new MinMax(min, max);
    }

    public static void main(String[] args) {
        MinMax r = minMax(new int[] {4, -2, 9, 7});
        System.out.println(r);
        System.out.println("range = " + (r.max() - r.min()));
    }
}
$ java MinMaxDemo.java
MinMax[min=-2, max=9]
range = 11

The record gives each value a name and type, and its generated toString makes the result easy to print while debugging.

Pass-by-value: primitives and objects

Java always copies the argument. For an object, what gets copied is the reference.

public class PassByValue {
    static void tryToDouble(int x) {
        x = x * 2;                       // changes the copy only
    }

    static void appendBang(StringBuilder sb) {
        sb.append("!");                  // follows the reference to the shared object
        sb = new StringBuilder("new");   // repoints the copy; caller unaffected
    }

    public static void main(String[] args) {
        int n = 5;
        tryToDouble(n);
        System.out.println("n = " + n);

        StringBuilder msg = new StringBuilder("hi");
        appendBang(msg);
        System.out.println("msg = " + msg);
    }
}
$ java PassByValue.java
n = 5
msg = hi!

n is untouched because tryToDouble changed its own copy. msg changed because the method used its copy of the reference to modify the one shared StringBuilder — but the reassignment to "new" was invisible to main.

Scope: variables live inside their braces

A variable exists from its declaration to the end of the block that contains it.

public class Scope {
    static int counter = 0;              // class field: visible in every method

    static void increment() {
        int step = 1;                    // local to increment()
        counter += step;
    }

    public static void main(String[] args) {
        increment();
        increment();
        for (int i = 0; i < 2; i++) {
            int doubled = i * 2;         // exists only inside the loop body
        }
        System.out.println(counter);
        System.out.println(step);
    }
}
$ java Scope.java
Scope.java:16: error: cannot find symbol
        System.out.println(step);
                           ^
  symbol:   variable step
  location: class Scope
1 error
error: compilation failed

step belongs to increment’s body, so main cannot see it — and the same would be true of i or doubled after the loop. Remove the last line and the program prints 2.

Varargs: any number of arguments

int... nums accepts zero or more ints and hands them to the method as an int[].

public class Varargs {
    static int sum(int... nums) {
        int total = 0;
        for (int n : nums) total += n;
        return total;
    }

    static String join(String sep, String... parts) {
        return String.join(sep, parts);
    }

    public static void main(String[] args) {
        System.out.println(sum());
        System.out.println(sum(1, 2, 3));
        System.out.println(sum(new int[] {10, 20}));
        System.out.println(join("-", "a", "b", "c"));
    }
}
$ java Varargs.java
0
6
30
a-b-c

A varargs parameter must come last, and a method can have only one. You can also pass an existing array directly, as the third call shows.

Recursion

A recursive method calls itself on a smaller input until it reaches a base case.

public class Recursion {
    static long factorial(int n) {
        if (n <= 1) return 1;            // base case
        return n * factorial(n - 1);     // smaller problem
    }

    static int fib(int n) {
        return n < 2 ? n : fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] args) {
        System.out.println("5! = " + factorial(5));
        System.out.println("20! = " + factorial(20));
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 10; i++) sb.append(fib(i)).append(' ');
        System.out.println(sb.toString().trim());
    }
}
$ java Recursion.java
5! = 120
20! = 2432902008176640000
0 1 1 2 3 5 8 13 21 34

factorial(5) waits on factorial(4), which waits on factorial(3), down to the base case; the results multiply on the way back up. 20! still fits in a long; 21! would overflow silently.

Static vs instance methods

A static method belongs to the class. An instance method runs on an object and can use its fields.

public class Counter {
    private int count;

    void increment() { count++; }        // instance: uses this.count

    int getCount() { return count; }

    static int add(int a, int b) {       // static: no object needed
        return a + b;
    }

    public static void main(String[] args) {
        System.out.println(Counter.add(3, 4));
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        c1.increment();
        c1.increment();
        c2.increment();
        System.out.println(c1.getCount() + " " + c2.getCount());
    }
}
$ java Counter.java
7
2 1

Each Counter object has its own count, so c1 and c2 differ. add touches no field, so it is called on the class. Calling increment() directly from static main would not compile — there is no this there.

Practice

  1. Write greet(String name) and an overload greet(String name, String title) so that greet("Nguyen", "Dr.") returns "Hello, Dr. Nguyen!", with the one-argument version delegating to the two-argument one.
Solution
public class Greet {
    static String greet(String name) {
        return greet(name, "");
    }

    static String greet(String name, String title) {
        String who = title.isEmpty() ? name : title + " " + name;
        return "Hello, " + who + "!";
    }

    public static void main(String[] args) {
        System.out.println(greet("Nguyen", "Dr."));
        System.out.println(greet("Sam"));
    }
}
$ java Greet.java
Hello, Dr. Nguyen!
Hello, Sam!

Java picks the overload from the number of arguments, and only one version holds the formatting logic.

  1. Write a varargs method maxOf(int first, int... rest) that returns the largest argument. Why is the separate first parameter useful?
Solution
public class MaxOf {
    static int maxOf(int first, int... rest) {
        int max = first;
        for (int n : rest) if (n > max) max = n;
        return max;
    }

    public static void main(String[] args) {
        System.out.println(maxOf(3, 7, 2, 9, 4));
        System.out.println(maxOf(-5));
    }
}
$ java MaxOf.java
9
-5

Requiring first means maxOf() with no arguments does not compile, so there is never an empty array with no maximum.

  1. Write divide(int a, int b) returning a record with both quotient and remainder, and print the result for 17 and 5.
Solution
public class Divide {
    record DivResult(int quotient, int remainder) {}

    static DivResult divide(int a, int b) {
        return new DivResult(a / b, a % b);
    }

    public static void main(String[] args) {
        DivResult r = divide(17, 5);
        System.out.println(r.quotient() + " remainder " + r.remainder());
        System.out.println(r);
    }
}
$ java Divide.java
3 remainder 2
DivResult[quotient=3, remainder=2]

Same shape as MinMax: one call, two named values back.

  1. tryToDouble in the pass-by-value example has no effect on the caller. Rewrite it so the caller ends up with 10, without changing how Java passes arguments.
Solution
public class DoubleIt {
    static int doubleIt(int x) {
        return x * 2;
    }

    public static void main(String[] args) {
        int value = 5;
        value = doubleIt(value);
        System.out.println(value);
    }
}
$ java DoubleIt.java
10

The method still sees only a copy; the change is that it returns the new value and the caller assigns it to its own variable.

  1. Write a recursive power(int base, int exp) and print 2^0, 2^10 and 3^4.
Solution
public class Power {
    static long power(int base, int exp) {
        if (exp == 0) return 1;
        return base * power(base, exp - 1);
    }

    public static void main(String[] args) {
        System.out.println(power(2, 0));
        System.out.println(power(2, 10));
        System.out.println(power(3, 4));
    }
}
$ java Power.java
1
1024
81

exp == 0 is the base case, and each call reduces exp by one, so the recursion always reaches it.

Frequently Asked Questions

Is Java pass-by-value or pass-by-reference?
Java is always pass-by-value. For a primitive the method gets a copy of the value; for an object it gets a copy of the reference. That means a method can change the fields of an object it was given, but it can never make the caller's variable point to a different object.
Can a Java method return multiple values?
Not directly; a method has exactly one return type. The cleanest way to return several values is to declare a small record, such as record MinMax(int min, int max), and return an instance of it. Arrays and lists also work but lose the names and types of each value.
Does Java support default parameter values?
No. Java has no default arguments. The usual substitute is overloading: write a shorter method that calls the full version with the default filled in. For many optional settings, a builder object is clearer.
What is the difference between a static and an instance method?
A static method belongs to the class and is called as ClassName.method(); it cannot read instance fields because there is no object. An instance method is called on an object and can read and change that object's fields through this.
What happens if a recursive method has no base case?
Each call adds a frame to the thread's stack, so without a base case the calls never stop and the JVM eventually throws StackOverflowError. Every recursive method needs a condition that returns without recursing, and each call must move closer to it.