Functional Programming
Last modified on Fri 17 Apr 2026

Functional programming gives a substantial advantage to Java programs. Code written in such a manner is concise, more expressive, with fewer moving parts, is easier to parallelize and is generally easier to understand than OO code. There is a challenge to change the way of thinking from imperative to declarative programming style - which pays off quickly.

Some of Java's tools for functional programming are lambda expressions, streams, functional interfaces, records, and pattern matching which will be explained in the rest of the chapter.

Lambda expression

Lambda expressions are shorter representations of anonymous classes with the following characteristics: * Anonymous - doesn't have an explicit name * Function - not tied to a particular class like a method * Passed around - can be passed as argument or stored in variable * Concise - no need to write a lot of boilerplate like in anonymous classes

Here is an example of writing an anonymous class in a more concise and shorter way using lambdas:

Before:

Comparator<Apple> byWeight = new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
};

After (with lambda expressions):

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

Functional interface

Any interface with a SAM (Single Abstract Method) is a functional interface. An implementation of that method can be treated as a lambda expression. Here are some functional interfaces which are most commonly used with their characteristics:

Predicate Consumer Supplier Comparator Function
Typical use-case filter collection of values perform action on each element provide results compare collection elements for sorting purpose map collection elements to get essential processing data
Method test(T t) accept(T t) get( ) compare(T t1, T t2) apply(T t)
Stream operation filter, allMatch, anyMatch forEach, peek generate max, min, sorted map, flatMap
Output type boolean void T int R

There are also functional interfaces which accept two arguments like BiConsumer, BiFunction, BiPredicate, BinaryOperator etc.

Stream

Streams are used for collections manipulation in declarative way. Manipulations are done by chaining intermediate operations and executing one of terminating operations which starts collection processing. Once consumed, stream can not be reused. Check Java Stream API for more details.

Intermediate operations Terminal operations
filter, map, sorted reduce, collect, forEach, anyMatch
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList.stream()
    .filter(s -> s.startsWith("c"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

// C1
// C2

Another way of creating Stream objects is by using static Stream.of() method.

Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);

// a1

This example contains the ifPresent() function which will be explained in Optional sub-chapter.

There is also an option of creating stream of numbers:

Stream.of(1.0, 2.0, 3.0)
    .mapToInt(Double::intValue)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);

// a1
// a2
// a3

Another way is using primitives' streams like IntStream, LongStream and DoubleStream.

IntStream.range(1, 4)
    .forEach(System.out::println);

// 1
// 2
// 3

Modern Stream Features

Optional<T>

The following example shows some additional functionality of the Optional class: Optional was designed to provide a better alternative to returning null from methods, not as a parameter type. According to Oracle's documentation, Optional should be used as a return type to indicate that a method may not return a value, helping to prevent null pointer exceptions.

It is used very often in functional programming, for example, when maximum value from collection is searched using stream because collection could initially be empty and maximum value would be null in that case.

Optional Features

Records and Pattern Matching

Records and pattern matching are powerful features introduced in recent Java versions that complement functional programming:

Records

Records are immutable data classes that automatically implement equals(), hashCode(), and toString():

public record User(String name, String email) {}

// Usage
User user = new User("John", "john@example.com");
System.out.println(user.name()); // Accessor method

Pattern Matching

Pattern matching simplifies type checking and casting:

// instanceof pattern matching
if (obj instanceof String s) {
    System.out.println(s.length());
}

// switch pattern matching
String result = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> "Weekend";
    case TUESDAY, THURSDAY -> "Weekday";
    case WEDNESDAY -> "Midweek";
};