Skip to content
Laviyx Learn

Try

Java course menu

Java Lambdas and Functional Interfaces: Predicate, Function, Consumer and Method References

Learn Java lambda expressions: syntax, functional interfaces, the built-in Predicate, Function, Consumer, Supplier and BiFunction, method references (Class::method), capturing effectively final variables, composing functions, and using lambdas with collections.

Advanced11 min readUpdated
On this page

A lambda expression is a short, unnamed function you can pass around like a value. Lambdas (Java 8+) make code that sorts, filters, transforms or reacts to events much shorter than writing a whole class for each behaviour.

Syntax#

Java
(parameters) -> expression
(parameters) -> { statements; return value; }
Java
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("Meera", "Asha", "Ravindra", "Jo"));

        names.sort((a, b) -> a.length() - b.length());   // two parameters, one expression
        System.out.println(names);

        names.removeIf(name -> name.length() < 3);        // one parameter: no parentheses needed
        System.out.println(names);

        names.forEach(name -> {                            // a block body
            String upper = name.toUpperCase();
            System.out.println(upper);
        });
    }
}
Output
[Jo, Asha, Meera, Ravindra]
[Asha, Meera, Ravindra]
ASHA
MEERA
RAVINDRA

Parameter types are usually inferred. Parentheses are optional for exactly one parameter, and an expression body needs no return.

Functional interfaces#

A lambda can be used wherever a functional interface is expected: an interface with exactly one abstract method. The lambda becomes the implementation of that method. Comparator (used by sort) and Predicate (used by removeIf) are functional interfaces.

java.util.function provides the common shapes, so you rarely need to define your own:

InterfaceMethodTakes → returnsTypical use
Predicate<T>testT → booleanConditions, filters
Function<T, R>applyT → RTransformations
Consumer<T>acceptT → nothingActions (printing, saving)
Supplier<T>getnothing → TLazy values, factories
BiFunction<T, U, R>applyT, U → RCombining two values
UnaryOperator<T>applyT → TSame-type transformations
Java
import java.util.function.*;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isEven = n -> n % 2 == 0;
        Function<String, Integer> length = s -> s.length();
        Consumer<String> shout = s -> System.out.println(s.toUpperCase() + "!");
        Supplier<String> greeting = () -> "Hello";
        BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
        UnaryOperator<String> twice = s -> s + s;

        System.out.println(isEven.test(4) + " " + isEven.test(7));
        System.out.println(length.apply("lambda"));
        shout.accept("hi");
        System.out.println(greeting.get() + " " + add.apply(2, 3) + " " + twice.apply("ab"));
    }
}
Output
true false
6
HI!
Hello 5 abab

Passing behaviour into a method#

Accepting a functional interface as a parameter lets callers decide what the method does:

Java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class Main {
    static List<Integer> filter(List<Integer> items, Predicate<Integer> condition) {
        List<Integer> result = new ArrayList<>();
        for (int item : items) {
            if (condition.test(item)) result.add(item);
        }
        return result;
    }

    public static void main(String[] args) {
        List<Integer> nums = List.of(5, 12, 7, 20, 3, 15);
        System.out.println(filter(nums, n -> n > 10));
        System.out.println(filter(nums, n -> n % 5 == 0));
    }
}
Output
[12, 20, 15]
[5, 20, 15]

The Streams API is built on exactly this idea.

Method references#

When a lambda only calls an existing method, a method reference (::) is even shorter:

KindMethod referenceEquivalent lambda
Static methodInteger::parseInts -> Integer.parseInt(s)
Instance method of the argumentString::toUpperCases -> s.toUpperCase()
Method of a particular objectSystem.out::printlnx -> System.out.println(x)
ConstructorArrayList::new() -> new ArrayList<>()
Java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> parse = Integer::parseInt;
        List<String> words = new ArrayList<>(List.of("kiwi", "apple", "fig"));

        System.out.println(parse.apply("42") + 1);
        words.replaceAll(String::toUpperCase);
        words.forEach(System.out::println);
    }
}
Output
43
KIWI
APPLE
FIG

Capturing variables: effectively final#

A lambda can use local variables from the surrounding method, but only if they are effectively final (never reassigned after initialization):

Java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        int limit = 10;   // never changed: effectively final
        List<Integer> nums = List.of(4, 11, 7, 20);
        long above = nums.stream().filter(n -> n > limit).count();
        System.out.println(above);
        // limit = 20;   // would make the lambda above fail to compile
    }
}
Output
2

To accumulate a result, do not modify a captured local variable; use a stream operation (sum, count, collect) or a field instead.

Composing functions#

Functional interfaces have default methods for combining them:

Java
import java.util.function.Function;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<String> notEmpty = s -> !s.isEmpty();
        Predicate<String> shortWord = s -> s.length() < 5;
        Predicate<String> valid = notEmpty.and(shortWord);

        Function<Integer, Integer> doubleIt = x -> x * 2;
        Function<Integer, Integer> addTen = x -> x + 10;

        System.out.println(valid.test("kiwi") + " " + valid.test("") + " " + valid.test("banana"));
        System.out.println(doubleIt.andThen(addTen).apply(5) + " " + doubleIt.compose(addTen).apply(5));
    }
}
Output
true false false
20 30

f.andThen(g) applies f then g; f.compose(g) applies g first. Predicates combine with and, or and negate.

Common mistakes#

Practice#

Practice exercise

Write a method applyAll(List<Integer> values, Function<Integer, Integer> f) that returns a new list with f applied to each value. Use it with a lambda that squares numbers and with the method reference Math::abs, on List.of(-3, 2, -1, 4).

Show solution
Java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public class ApplyAll {
    static List<Integer> applyAll(List<Integer> values, Function<Integer, Integer> f) {
        List<Integer> result = new ArrayList<>();
        for (int v : values) {
            result.add(f.apply(v));
        }
        return result;
    }

    public static void main(String[] args) {
        List<Integer> values = List.of(-3, 2, -1, 4);
        System.out.println(applyAll(values, x -> x * x));
        System.out.println(applyAll(values, Math::abs));
    }
}
Output
[9, 4, 1, 16]
[3, 2, 1, 4]

Key takeaway#

Finished this lesson?