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

Functions (Methods) in Java

Master Java methods — declarations, static methods, pass-by-value, varargs, recursion, lambda expressions, method references and functional interfaces.

By SkillByExample · Updated

Methods — The Building Blocks of Behaviour

A method is a named block of code that performs a specific task. Methods let you write logic once and reuse it, making programs easier to read, test, and maintain. Every runnable example here is a single file run with the JDK 21 source launcher (java File.java).

// Basic method anatomy
returnType methodName(parameterType paramName, ...) {
    // body
    return value; // required if returnType is not void
}
public class Calculator {

    // void — returns nothing
    public void printResult(int result) {
        System.out.println("Result: " + result);
    }

    // returns an int
    public int add(int a, int b) {
        return a + b;
    }

    // returns a double
    public double divide(double numerator, double denominator) {
        if (denominator == 0) {
            throw new ArithmeticException("Cannot divide by zero");
        }
        return numerator / denominator;
    }

    public static void main(String[] args) {
        Calculator calc = new Calculator();
        int sum = calc.add(10, 25);
        calc.printResult(sum);
        System.out.println(calc.divide(10.0, 4.0));
    }
}
$ java Calculator.java
Result: 35
2.5

printResult is void, so it does its work by printing; add and divide return values the caller can store or print. divide uses double parameters, which is why 10.0 / 4.0 gives 2.5 rather than the 2 that int division would.

Access Modifiers and Static Methods

ModifierVisible to
publicEveryone
protectedSame package + subclasses
privateSame class only
(none)Same package only
public class MathUtilsDemo {
    public static void main(String[] args) {
        System.out.println(MathUtils.square(7));
        System.out.println(MathUtils.max(12, 8));
        System.out.println(MathUtils.parity(6));
    }
}

class MathUtils {

    // static — belongs to the class, not an instance
    // call as MathUtils.square(5), not new MathUtils().square(5)
    public static int square(int n) {
        return n * n;
    }

    public static int max(int a, int b) {
        return a > b ? a : b;
    }

    // private helper — internal use only
    private static boolean isEven(int n) {
        return n % 2 == 0;
    }

    public static String parity(int n) {
        return isEven(n) ? "even" : "odd";
    }
}
$ java MathUtilsDemo.java
49
12
even

MathUtils.isEven is private, so only code inside MathUtils can call it; MathUtilsDemo uses the public parity method instead. The static methods are called on the class name — no new MathUtils() anywhere.

Parameters — Pass-by-Value

Java is always pass-by-value. For primitives, the method gets a copy. For objects, the method gets a copy of the reference (it can mutate the object’s state, but cannot reassign the original variable).

public class PassByValueDemo {

    // primitives — original is unchanged
    static void doubleIt(int x) {
        x = x * 2;
        System.out.println("Inside: " + x);
    }

    // object reference — can mutate the object
    static void addItem(java.util.List<String> list, String item) {
        list.add(item); // modifies the actual list
    }

    public static void main(String[] args) {
        int n = 10;
        doubleIt(n);
        System.out.println("After: " + n);

        var names = new java.util.ArrayList<String>();
        addItem(names, "Alice");
        System.out.println(names);
    }
}
$ java PassByValueDemo.java
Inside: 20
After: 10
[Alice]

n stays 10 after doubleIt(n) because x = x * 2 only reassigns the method’s local copy — but addItem mutates the actual ArrayList object through its copied reference, which is why names really does end up with "Alice" in it. Same pass-by-value rule, two different-looking outcomes.

Varargs — Variable Argument Count

public class Stats {

    // varargs — caller can pass any number of ints
    public static int sum(int... numbers) {
        int total = 0;
        for (int n : numbers) total += n;
        return total;
    }

    public static double average(double... values) {
        if (values.length == 0) return 0.0;
        double total = 0;
        for (double v : values) total += v;
        return total / values.length;
    }

    public static void main(String[] args) {
        System.out.println(sum(1, 2, 3));
        System.out.println(sum(10, 20, 30, 40));
        System.out.println(average(4.0, 7.5, 2.5));
    }
}
$ java Stats.java
6
100
4.666666666666667

Inside the method, int... numbers is an ordinary int[], so the same sum accepts three or four arguments. A varargs parameter must come last in the parameter list.

Recursion

A method that calls itself. Always needs a base case to stop.

public class Recursion {

    // factorial: n! = n * (n-1)!  base case: 0! = 1
    public static long factorial(int n) {
        if (n <= 0) return 1;        // base case
        return n * factorial(n - 1); // recursive case
    }

    // Fibonacci with memoization (avoid exponential blowup)
    private static java.util.Map<Integer, Long> memo = new java.util.HashMap<>();

    public static long fib(int n) {
        if (n <= 1) return n;
        if (memo.containsKey(n)) return memo.get(n);
        long result = fib(n - 1) + fib(n - 2);
        memo.put(n, result);
        return result;
    }

    public static void main(String[] args) {
        System.out.println(factorial(10));
        System.out.println(fib(50));
    }
}
$ java Recursion.java
3628800
12586269025

fib(50) without memoization would make roughly 40 billion recursive calls — the memo map is what turns an exponential-time function into a linear one, caching each fib(n) the first time it’s computed.

Functional Interfaces

A functional interface has exactly one abstract method. Java’s java.util.function package provides the most common ones:

import java.util.function.*;

public class FunctionalDemo {
    public static void main(String[] args) {

        // Predicate<T> — takes T, returns boolean
        Predicate<String> isLong = s -> s.length() > 5;
        System.out.println(isLong.test("Hi"));
        System.out.println(isLong.test("Hello World"));

        // Function<T, R> — takes T, returns R
        Function<String, Integer> wordCount = s -> s.split("\\s+").length;
        System.out.println(wordCount.apply("one two three"));

        // Consumer<T> — takes T, returns void
        Consumer<String> print = s -> System.out.println(">> " + s);
        print.accept("Hello");

        // Supplier<T> — takes nothing, returns T
        Supplier<Double> random = Math::random;
        System.out.println("random: " + random.get() + " (varies)");

        // BiFunction<T, U, R> — takes two args, returns R
        BiFunction<Integer, Integer, Integer> power = (base, exp) -> {
            int result = 1;
            for (int i = 0; i < exp; i++) result *= base;
            return result;
        };
        System.out.println(power.apply(2, 10));

        // Composing functions
        Function<Integer, Integer> times2       = x -> x * 2;
        Function<Integer, Integer> plus3        = x -> x + 3;
        Function<Integer, Integer> times2Plus3  = times2.andThen(plus3);
        System.out.println(times2Plus3.apply(5));
    }
}
$ java FunctionalDemo.java
false
true
3
>> Hello
random: 0.8164651954186748 (varies)
1024
13

The random: line prints a different value every run — Math::random is a method reference to a random source, unlike everything else on this page. times2.andThen(plus3) applies left to right: 5 * 2 = 10, then 10 + 3 = 13, not (5 + 3) * 2.

Lambda Expressions

Lambdas are compact anonymous functions assignable to any functional interface:

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class LambdaDemo {

    @FunctionalInterface
    interface Transformer<T> {
        T transform(T input);
    }

    public static void main(String[] args) {

        List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob", "Dave"));

        // Lambda — concise
        names.sort((a, b) -> a.compareTo(b));

        // Method reference — even shorter
        names.sort(String::compareTo);

        System.out.println(names);

        // Custom functional interface
        Transformer<String> shout = s -> s.toUpperCase() + "!";
        System.out.println(shout.transform("hello"));

        // Predicate composition
        Predicate<Integer> positive        = n -> n > 0;
        Predicate<Integer> even            = n -> n % 2 == 0;
        Predicate<Integer> positiveAndEven = positive.and(even);

        List.of(-2, 0, 3, 4, 7, 8).stream()
            .filter(positiveAndEven)
            .forEach(System.out::println);
    }
}
$ java LambdaDemo.java
[Alice, Bob, Charlie, Dave]
HELLO!
4
8

-2 and 0 fail positive; 3 and 7 fail even; only 4 and 8 satisfy both predicates positiveAndEven chains together with and().

Method References

A cleaner syntax for lambdas that simply delegate to an existing method:

FormSyntaxLambda equivalent
Static methodClassName::staticMethodx -> ClassName.staticMethod(x)
Instance on specific objectinstance::methodx -> instance.method(x)
Instance on arbitrary objectClassName::instanceMethod(obj, x) -> obj.method(x)
ConstructorClassName::newargs -> new ClassName(args)
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class MethodRefDemo {

    static int doubleIt(int n) { return n * 2; }

    record Person(String name, int age) {}

    public static void main(String[] args) {

        // 1. Static method reference
        Function<Integer, Integer> dbl = MethodRefDemo::doubleIt;
        System.out.println(dbl.apply(7));

        // 2. Instance method on a particular instance
        String prefix = "Hello, ";
        Function<String, String> greet = prefix::concat;
        System.out.println(greet.apply("World"));

        // 3. Instance method on an arbitrary instance of the type
        Function<String, String> upper = String::toUpperCase;
        System.out.println(upper.apply("java"));

        // 4. Constructor reference
        BiFunction<String, Integer, Person> makePerson = Person::new;
        Person p = makePerson.apply("Alice", 30);
        System.out.println(p);

        // Real-world: print a list
        List<String> words = List.of("foo", "bar", "baz");
        words.forEach(System.out::println);

        // Collect to uppercase list
        List<String> upperWords = words.stream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());
        System.out.println(upperWords);
    }
}
$ java MethodRefDemo.java
14
Hello, World
JAVA
Person[name=Alice, age=30]
foo
bar
baz
[FOO, BAR, BAZ]

Person[name=Alice, age=30] is a record’s auto-generated toString() — records get it, plus equals()/hashCode(), for free from the two-field declaration alone.

Writing Good Methods

Keep each method to one job, name it for what it does, and group long parameter lists into a record. This small checkout follows all three:

import java.util.List;

public class Checkout {
    record OrderRequest(String customer, List<Double> prices, double discountPercent) {}

    static void validate(OrderRequest r) {
        if (r.prices().isEmpty()) throw new IllegalArgumentException("empty order for " + r.customer());
    }

    static double subtotal(List<Double> prices) {
        return prices.stream().mapToDouble(Double::doubleValue).sum();
    }

    static double applyDiscount(double amount, double percent) {
        return amount * (1 - percent / 100);
    }

    static String placeOrder(OrderRequest r) {
        validate(r);
        double total = applyDiscount(subtotal(r.prices()), r.discountPercent());
        return String.format("%s pays %.2f", r.customer(), total);
    }

    public static void main(String[] args) {
        System.out.println(placeOrder(new OrderRequest("Alice", List.of(19.99, 5.01), 10)));
        System.out.println(placeOrder(new OrderRequest("Bob", List.of(), 0)));
    }
}
$ java Checkout.java
Alice pays 22.50
Exception in thread "main" java.lang.IllegalArgumentException: empty order for Bob
	at Checkout.validate(Checkout.java:7)
	at Checkout.placeOrder(Checkout.java:19)
	at Checkout.main(Checkout.java:26)

placeOrder reads as a summary of the steps, and each helper can be tested on its own. Bob’s empty order fails in validate, before any total is computed. The OrderRequest record replaces three loose parameters, so a caller can’t swap the customer and the discount by accident, and adding a field later doesn’t change every method signature.

Practice

  1. Write a varargs method maxOf(int... nums) that returns the largest argument, and call it with five numbers and with one.
Solution
public class MaxOf {
    static int maxOf(int... nums) {
        int max = nums[0];
        for (int n : nums) 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(42));
        System.out.println(maxOf());
    }
}
$ java MaxOf.java
9
42
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0
	at MaxOf.maxOf(MaxOf.java:3)
	at MaxOf.main(MaxOf.java:11)

Inside the method, varargs is just an int[]. Calling it with no arguments is legal and passes an empty array, so nums[0] fails; the signature maxOf(int first, int... rest) makes “at least one” a compile-time rule.

  1. Write a recursive gcd(a, b) using the Euclidean algorithm: gcd(a, 0) = a, otherwise gcd(a, b) = gcd(b, a % b).
Solution
public class Gcd {
    static int gcd(int a, int b) {
        if (b == 0) return a;
        return gcd(b, a % b);
    }

    public static void main(String[] args) {
        System.out.println(gcd(48, 18));
        System.out.println(gcd(17, 5));
    }
}
$ java Gcd.java
6
1

One base case (b == 0) and one recursive case that always moves toward it, since a % b is smaller than b.

  1. Combine two Predicate<Integer>s with or() and print the result for -3, -2, 4, 5 and 7.
Solution
import java.util.List;
import java.util.function.Predicate;

public class OrPredicate {
    public static void main(String[] args) {
        Predicate<Integer> negative = n -> n < 0;
        Predicate<Integer> even = n -> n % 2 == 0;
        Predicate<Integer> negativeOrEven = negative.or(even);
        List.of(-3, -2, 4, 5, 7).forEach(n -> System.out.println(n + ": " + negativeOrEven.test(n)));
    }
}
$ java OrPredicate.java
-3: true
-2: true
4: true
5: false
7: false

or() needs only one side to be true: -3 passes negative, 4 passes even, -2 passes both.

  1. Sort ["kiwi", "apple", "fig"] by length with Comparator.comparing and a method reference, then by length descending.
Solution
import java.util.*;

public class ByLength {
    public static void main(String[] args) {
        List<String> words = new ArrayList<>(List.of("kiwi", "apple", "fig"));
        words.sort(Comparator.comparing(String::length));
        System.out.println(words);
        words.sort(Comparator.comparing(String::length).reversed());
        System.out.println(words);
    }
}
$ java ByLength.java
[fig, kiwi, apple]
[apple, kiwi, fig]

String::length is the “instance method on an arbitrary object” form: Comparator.comparing calls it on each element being compared.

  1. Count how many times fib actually computes a value (not served from memo) when you call fib(50) twice.
Solution
import java.util.HashMap;
import java.util.Map;

public class FibCount {
    static Map<Integer, Long> memo = new HashMap<>();
    static int computed = 0;

    static long fib(int n) {
        if (n <= 1) return n;
        if (memo.containsKey(n)) return memo.get(n);
        computed++;
        long result = fib(n - 1) + fib(n - 2);
        memo.put(n, result);
        return result;
    }

    public static void main(String[] args) {
        System.out.println(fib(50) + " after " + computed + " computations");
        System.out.println(fib(50) + " after " + computed + " computations");
    }
}
$ java FibCount.java
12586269025 after 49 computations
12586269025 after 49 computations

Each of fib(2)…fib(50) is computed once; the static memo survives between calls, so the second call does no new work.

Frequently Asked Questions

What is the difference between a method and a function in Java?
In Java, all functions are methods — they must belong to a class. The word 'function' is often used informally, but technically Java has methods. The distinction matters when comparing with languages like Python or JavaScript where functions can exist outside classes.
When should I use a lambda instead of a named method?
Use a lambda for short, one-off logic passed to a method (sorting, filtering, event handling). Use a named method when the logic is reused in multiple places, needs unit testing by itself, or is more than 2-3 lines — named methods are easier to read and debug.
What is a functional interface?
A functional interface is any interface with exactly one abstract method. Examples from java.util.function: Predicate<T>, Function<T,R>, Consumer<T>, Supplier<T>. The @FunctionalInterface annotation enforces this at compile time. Lambdas can be assigned to any functional interface.
Can Java methods return multiple values?
Not directly. Common workarounds: return an array or List, return a custom object, return a Map.Entry, or use a record (Java 16+) like record Pair<A,B>(A first, B second) {} which is the cleanest approach.