Java Pass Function as Parameter — Complete Guide with Code Examples

Java Pass Function as Parameter — Complete Guide with Code Examples

To pass a function as a parameter in Java, you use a functional interface together with a lambda expression or a method reference. Java does not let you pass raw methods directly, so the function must be wrapped in a type such as Function, Predicate, Consumer, or your own custom functional interface.

This is one of the most important ideas in modern Java because it lets the same method accept different behavior at runtime. That is exactly how streams, callbacks, validators, mappers, and strategy-like business rules work.

Quick answer

  • Use Function<T,R> when input and output are different
  • Use Predicate<T> for true/false checks
  • Use Consumer<T> when you want a side effect and no return value
  • Use Supplier<T> when you want to return a value with no input
  • Use a custom @FunctionalInterface only when built-in interfaces do not fit

Key Benefits at a Glance

  • Cleaner Code: Replace verbose anonymous classes with short, readable lambdas.
  • More Flexibility: Pass different behavior into the same method at runtime.
  • Better Reuse: Reuse one method with different logic instead of duplicating code.
  • Modern Java APIs: This is the core idea behind Stream API methods like map(), filter(), and forEach().
  • Easier Composition: Combine small functions into larger workflows with built-in functional interfaces.
GoalInterface to useExample
Transform a valueFunction<T,R>s -> s.toUpperCase()
Check a conditionPredicate<T>s -> s.isBlank()
Perform an actionConsumer<T>System.out::println
Return a value with no inputSupplier<T>() -> "default"

Introduction to passing functions as parameters in Java

Passing a function as a parameter means your method receives behavior, not just data. Instead of hardcoding what the method should do internally, you give it logic from the outside.

Here is the minimal pattern before anything else:

import java.util.function.Function;

// Method accepts a function: String -> String
public String transform(String input, Function<String, String> fn) {
    return fn.apply(input);
}

// Call with a lambda
String result = transform("hello", s -> s.toUpperCase()); // HELLO

// Or with a method reference
String result2 = transform("hello", String::toUpperCase); // HELLO

That is the full pattern: define a method that accepts a functional interface, then pass behavior using either a lambda or a method reference. The rest of this guide shows when to use each interface, how lambdas work, when method references are cleaner, and what mistakes to avoid in production code.

  • Functions are passed through functional interfaces
  • Lambda expressions and method references are the two main syntaxes
  • Built-in interfaces cover most real-world cases
  • This pattern powers the Stream API and many callback-based APIs
  • Works in any Java 8+ project with no extra dependencies

How Java handled this before and after Java 8

Before Java 8, Java developers passed behavior using interfaces and anonymous inner classes. Since Java 8, lambdas and method references made the same pattern much shorter and easier to read.

ApproachSyntax sizeReadability
Anonymous inner classVerboseLower
Lambda expressionShortHigh
Method referenceShortestVery high
interface Calculator {
    int calculate(int a, int b);
}

public void processNumbers(int x, int y, Calculator calc) {
    System.out.println(calc.calculate(x, y));
}

// Before Java 8
processNumbers(5, 3, new Calculator() {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
});

// Java 8+
processNumbers(5, 3, (a, b) -> a + b);
  • Before lambdas, even small callbacks required a lot of boilerplate
  • Anonymous inner classes worked, but they hid simple logic inside object syntax
  • Java 8 made behavior parameterization much more practical for everyday code

Functional interfaces: the foundation of function parameters

A functional interface is an interface with exactly one abstract method. This is the type that a lambda expression or method reference is assigned to. In practice, this means you do not pass a raw function in Java — you pass an object whose type is a functional interface.

In most cases, start with the built-in interfaces from java.util.function before creating your own.

InterfaceMethodUse caseExample
Function<T,R>R apply(T t)Transform input to outputs -> s.length()
Predicate<T>boolean test(T t)Check a conditions -> s.isEmpty()
Consumer<T>void accept(T t)Perform an actionSystem.out::println
Supplier<T>T get()Return a value with no input() -> new ArrayList<>()
BiFunction<T,U,R>R apply(T t, U u)Two inputs, one output(a, b) -> a + b
UnaryOperator<T>T apply(T t)Same type in and outs -> s.trim()
import java.util.function.*;

public class BuiltInInterfacesDemo {

    public static Integer applyFunction(String s, Function<String, Integer> fn) {
        return fn.apply(s);
    }

    public static boolean check(String s, Predicate<String> predicate) {
        return predicate.test(s);
    }

    public static void process(String s, Consumer<String> action) {
        action.accept(s);
    }

    public static String getDefault(Supplier<String> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        System.out.println(applyFunction("hello", String::length)); // 5
        System.out.println(check("", String::isEmpty));             // true
        process("world", System.out::println);                      // world
        System.out.println(getDefault(() -> "fallback value"));     // fallback value
    }
}
  • Function<T,R> transforms data
  • Predicate<T> checks a condition
  • Consumer<T> performs an action without returning a value
  • Supplier<T> returns a value without input
  • BiFunction<T,U,R> is useful when one argument is not enough
  • Prefer built-in interfaces when their signatures already fit
  • Use @FunctionalInterface on custom interfaces for compile-time safety
  • Predicate is what filter() expects in streams
  • Function is what map() expects in streams
  • Default and static methods are allowed inside functional interfaces

Creating your own functional interfaces

Create a custom functional interface only when the built-in ones do not describe your method signature or your domain clearly enough. In many cases, Function, Predicate, or Consumer is already enough.

@FunctionalInterface
public interface StringProcessor {
    String process(String input, int maxLength);

    default String processWithPrefix(String input, int maxLength) {
        return "Processed: " + process(input, maxLength);
    }
}

public void handleString(String text, StringProcessor processor) {
    String result = processor.process(text, 10);
    System.out.println(result);
}

handleString("Hello World", (s, len) -> s.substring(0, Math.min(s.length(), len)));
// Output: Hello Worl
  • Use @FunctionalInterface on every interface intended for lambdas
  • Name custom interfaces after what the behavior does, for example ValidationRule or PriceCalculator
  • Check java.util.function first before inventing your own type
  • Keep the single abstract method narrow and clear
  • Use default methods only for convenience, not for the core contract

How parameter passing works in Java

Java is always pass-by-value. When you pass a lambda or method reference, Java passes a copy of the reference to the function object. The function itself is not copied, and captured local variables must be effectively final.

What you passWhat Java copies
PrimitiveThe actual value
ObjectThe reference value
Lambda / method referenceThe reference to the function object

When using method references like MyClass::staticMethod, the distinction between static and instance context matters. See Static vs Non-Static in Java for a deeper explanation.

Lambda expressions: the modern approach

A lambda is an anonymous function defined inline. The general syntax is (parameters) -> expression or (parameters) -> { statements; }. The compiler usually infers the type from the surrounding functional interface.

// Single expression
processNumbers(5, 3, (a, b) -> a + b);

// Block body
processNumbers(5, 3, (a, b) -> {
    int sum = a + b;
    System.out.println("Computing: " + a + " + " + b);
    return sum;
});

// No parameters
Runnable r = () -> System.out.println("Running!");

// One parameter
Consumer<String> printer = s -> System.out.println(s);

// Type inferred
Function<String, Integer> fn1 = s -> s.length();

// Explicit type
Function<String, Integer> fn2 = (String s) -> s.length();

Lambdas can capture variables from the enclosing scope, but only if those variables are effectively final. In other words, you can read them, but you cannot reassign them later.

String prefix = "Hello, ";

Function<String, String> greeter = name -> prefix + name;
System.out.println(greeter.apply("Alice")); // Hello, Alice

// This would not compile if prefix were reassigned later
  • Keep lambdas short and focused
  • If a lambda becomes hard to read, extract it into a named method
  • Captured local variables must be effectively final
  • Single-expression lambdas need no braces and no return
  • Multi-statement lambdas use braces and explicit return
  • Type inference usually makes explicit parameter types unnecessary
  • Lambdas are objects that implement functional interfaces
  • You can store, return, and pass lambdas just like other objects

Method references: cleaner syntax for existing methods

A method reference is a shorter way to write a lambda that only calls an existing method. For example, instead of s -> s.toUpperCase(), you can write String::toUpperCase. Use method references when they make the code clearer, not just shorter.

TypeSyntaxEquivalent lambda
Static methodClassName::staticMethodx -> ClassName.staticMethod(x)
Instance method on a specific objectinstance::methodx -> instance.method(x)
Instance method on an arbitrary objectClassName::instanceMethodx -> x.instanceMethod()
ConstructorClassName::new() -> new ClassName()
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class MethodReferencesDemo {

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

    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie");

        // Static method reference
        List<Integer> doubled = List.of(1, 2, 3).stream()
            .map(MethodReferencesDemo::doubleIt)
            .collect(Collectors.toList());

        // Specific object method reference
        String expected = "Hello";
        Predicate<String> isHello = expected::equals;

        // Arbitrary object method reference
        List<String> upper = names.stream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());

        // Constructor reference
        Supplier<ArrayList<String>> listFactory = ArrayList::new;
        ArrayList<String> newList = listFactory.get();
    }
}
  • Prefer method references over lambdas when the lambda only calls one existing method
  • Use ClassName::instanceMethod often in streams for cleaner code
  • Constructor references work well with Supplier and factory-like code
  • Do not force method references when a lambda is easier to understand

Real-world examples: streams, callbacks, and strategy pattern

The idea becomes much clearer once you see it in real code. In modern Java projects, functions are most often passed into streams, callback handlers, validators, mappers, and strategy-like business rules.

Passing functions to streams

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

public class StreamFunctionsDemo {

    public static void main(String[] args) {
        List<String> words = List.of("java", "stream", "lambda", "filter", "map");

        // filter takes Predicate<T>
        List<String> longWords = words.stream()
            .filter(w -> w.length() > 4)
            .collect(Collectors.toList());

        // map takes Function<T, R>
        List<Integer> lengths = words.stream()
            .map(String::length)
            .collect(Collectors.toList());

        // forEach takes Consumer<T>
        words.stream()
            .filter(w -> w.startsWith("l"))
            .forEach(System.out::println);

        // sorted takes Comparator
        List<String> sorted = words.stream()
            .sorted(Comparator.comparingInt(String::length))
            .collect(Collectors.toList());
    }
}

Callback pattern

import java.util.function.Consumer;

public class FileProcessor {

    public void processFile(String filename, Consumer<String> onSuccess, Consumer<Exception> onError) {
        try {
            String content = readFile(filename);
            onSuccess.accept(content);
        } catch (Exception e) {
            onError.accept(e);
        }
    }

    private String readFile(String filename) {
        return "file content of " + filename;
    }
}

FileProcessor processor = new FileProcessor();
processor.processFile(
    "data.csv",
    content -> System.out.println("Read: " + content),
    err -> System.err.println("Failed: " + err.getMessage())
);

If your callback processes a CSV file, also see Java read CSV file for correct parsing strategies.

Strategy pattern with functional interfaces

import java.util.function.Function;

public class PriceCalculator {

    public double calculate(double basePrice, Function<Double, Double> discountStrategy) {
        return discountStrategy.apply(basePrice);
    }

    public static void main(String[] args) {
        PriceCalculator calc = new PriceCalculator();

        Function<Double, Double> noDiscount = price -> price;
        Function<Double, Double> tenPercent = price -> price * 0.90;
        Function<Double, Double> flatFifteen = price -> price - 15.0;

        System.out.println(calc.calculate(100.0, noDiscount));  // 100.0
        System.out.println(calc.calculate(100.0, tenPercent));  // 90.0
        System.out.println(calc.calculate(100.0, flatFifteen)); // 85.0
    }
}

Common mistakes and how to avoid them

Most bugs here do not come from syntax. They come from readability problems, captured mutable state, checked exceptions, and using custom interfaces where built-in ones already exist. These are the mistakes that show up most often in real Java codebases.

MistakeProblemFix
Mutating captured variablesCompile error: variable must be effectively finalUse a wrapper like AtomicInteger
Ignoring checked exceptions in lambdasLambdas cannot throw checked exceptions unless the interface allows itWrap or convert them explicitly
Creating unnecessary custom interfacesReinventing Function, Predicate, or ConsumerCheck java.util.function first
Complex lambda bodiesHarder to read, test, and debugExtract to a named method and use a method reference
Ignoring thread safetyShared mutable state can cause race conditionsKeep lambdas stateless or use thread-safe types
import java.util.concurrent.atomic.AtomicInteger;

// MISTAKE: trying to modify a captured variable
// int count = 0;
// list.forEach(item -> count++); // compile error

// FIX
AtomicInteger count = new AtomicInteger(0);
list.forEach(item -> count.incrementAndGet());

// MISTAKE: complex lambda
list.stream()
    .map(s -> {
        String trimmed = s.trim();
        String upper = trimmed.toUpperCase();
        return upper.replace(" ", "_");
    });

// FIX: extract to named method
list.stream().map(MyUtils::normalize);

public class MyUtils {
    public static String normalize(String s) {
        return s.trim().toUpperCase().replace(" ", "_");
    }
}
  • Keep lambdas short enough to understand at a glance
  • Do not capture mutable local variables
  • Use method references when they improve readability
  • Use built-in interfaces unless there is a real reason not to

Common operations inside lambdas often include string transformations and parsing. See Java split string by delimiter and Java OutputStream to String for related patterns.

Building real Java backends? Passing functions as parameters is not just a language trick. It is one of the patterns behind cleaner services, reusable validators, DTO mappers, file processors, and configurable business rules.

More Java Guides

Frequently Asked Questions

Use a functional interface as the parameter type, then pass a lambda expression or method reference as the argument. For example, void run(Runnable r) { r.run(); } can be called as run(() -> System.out.println("hi")). Built-in interfaces like Function<T,R>, Predicate<T>, and Consumer<T> cover most use cases.

Lambda expressions are anonymous functions with the syntax (params) -> body. They implement functional interfaces inline, which makes passing behavior concise and readable. This is why stream code like filter(s -> s.length() > 3) is possible.

Method references use the :: operator to point to an existing method without calling it yet. For example, String::toUpperCase is equivalent to s -> s.toUpperCase(). Use them when the lambda body is only a direct method call and the result is clearer.

Functional interfaces have exactly one abstract method and are the type bridge that allows lambdas and method references to be used as function parameters. Common examples are Function, Predicate, Consumer, and Supplier.

A lambda defines behavior inline, for example s -> s.toUpperCase(). A method reference points to an existing method, for example String::toUpperCase. Method references are shorter, but only work when the lambda body is just a direct method call. If the logic is more complex, use a lambda instead.

Create a custom functional interface only when the built-in interfaces in java.util.function do not describe your method signature or domain clearly enough. If Function, Predicate, Consumer, or Supplier already fits, use the built-in type instead.

Prefer lambdas for short inline logic and method references for existing named methods. Use built-in interfaces before creating custom ones. Keep lambdas stateless when possible. Extract complex lambda bodies into named methods for readability and testing. Avoid capturing mutable local state.

Before Java 8, Java passed behavior using anonymous inner classes that implemented interfaces such as Runnable, Comparator, or custom callback interfaces. This worked, but required much more boilerplate than lambdas and method references.

Yes. A method can accept multiple functional interface parameters, each with a different role. For example, one callback for success and another for error handling. This is common in file processing, validation pipelines, and asynchronous workflows.