Lambdas & Functional Interfaces

Function & BiFunction

15 min Lesson 5 of 13

Function & BiFunction

In the previous lesson you worked with Predicate, which always returns a boolean. The Function interface generalises that idea: it takes a value of one type and transforms it into a value of a potentially different type. This is the interface behind virtually every data-mapping, parsing, and conversion step you write with lambdas.

The Function interface

java.util.function.Function<T, R> has one abstract method:

@FunctionalInterface public interface Function<T, R> { R apply(T t); }

T is the input type; R is the result type. They can be the same, but they do not have to be. A few quick examples:

// String → Integer Function<String, Integer> length = s -> s.length(); System.out.println(length.apply("hello")); // 5 // Integer → String Function<Integer, String> intToStr = n -> "value=" + n; System.out.println(intToStr.apply(42)); // value=42 // String → String (same type in and out) Function<String, String> shout = s -> s.toUpperCase() + "!"; System.out.println(shout.apply("java")); // JAVA!
When to reach for Function vs a plain method: Use Function when you want to pass a transformation as a parameter, store it in a variable, or combine it with other functions. If you just need to call the logic once in place, a regular method is simpler.

Composing with andThen

Function has a default method andThen(Function after) that chains two functions together: this function runs first, then after receives its result.

Function<String, String> trim = String::trim; Function<String, String> upper = String::toUpperCase; // pipeline: trim the input, then convert to upper case Function<String, String> normalise = trim.andThen(upper); System.out.println(normalise.apply(" hello world ")); // HELLO WORLD

You can chain as many functions as you like:

Function<String, Integer> countWords = ((Function<String, String>) String::trim) .andThen(s -> s.replaceAll("\\s+", " ")) .andThen(s -> s.split(" ")) .andThen(parts -> parts.length); System.out.println(countWords.apply(" one two three ")); // 3
Read andThen as a pipeline: f.andThen(g) means "do f, then pass its output to g". The data flows left to right, matching how you would read a sentence. This makes multi-step transformations very readable.

Composing with compose

compose(Function before) is the mirror of andThen: before runs first, and this function receives its result.

Function<Integer, Integer> doubleIt = x -> x * 2; Function<Integer, Integer> addTen = x -> x + 10; // compose: addTen runs first, then doubleIt Function<Integer, Integer> addThenDouble = doubleIt.compose(addTen); System.out.println(addThenDouble.apply(5)); // (5+10)*2 = 30 // andThen: doubleIt runs first, then addTen Function<Integer, Integer> doubleThenAdd = doubleIt.andThen(addTen); System.out.println(doubleThenAdd.apply(5)); // (5*2)+10 = 20
andThen vs compose — easy to mix up: f.andThen(g) means f → g. f.compose(g) means g → f (g goes first). In practice most developers reach for andThen because the left-to-right reading order matches the execution order. Reserve compose for cases where you are decorating an existing function from the outside.

The BiFunction interface

Sometimes a transformation needs two inputs. java.util.function.BiFunction<T, U, R> covers that:

@FunctionalInterface public interface BiFunction<T, U, R> { R apply(T t, U u); }

A practical example — formatting a full name from separate first and last name strings:

BiFunction<String, String, String> fullName = (first, last) -> first + " " + last; System.out.println(fullName.apply("Ada", "Lovelace")); // Ada Lovelace

Another example — combining a label with a numeric value for display:

BiFunction<String, Integer, String> label = (name, score) -> name + ": " + score + " pts"; System.out.println(label.apply("Alice", 95)); // Alice: 95 pts

BiFunction also has andThen (but NOT compose), so you can post-process the result:

BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; Function<Integer, String> format = n -> "Result: " + n; var multiplyAndFormat = multiply.andThen(format); System.out.println(multiplyAndFormat.apply(6, 7)); // Result: 42

UnaryOperator and BinaryOperator — convenient specialisations

When the input and output types are the same, Java provides two shorthand interfaces:

  • UnaryOperator<T> extends Function<T, T> — one argument, same return type.
  • BinaryOperator<T> extends BiFunction<T, T, T> — two arguments, same return type.
import java.util.function.UnaryOperator; import java.util.function.BinaryOperator; UnaryOperator<String> exclaim = s -> s + "!"; System.out.println(exclaim.apply("wow")); // wow! BinaryOperator<Integer> add = (a, b) -> a + b; System.out.println(add.apply(3, 4)); // 7

Use these instead of Function<T, T> or BiFunction<T, T, T> — they are more expressive and certain APIs (like List.replaceAll) expect them by type.

Putting it together — a reusable transformation pipeline

Here is a realistic mini-example: a simple data normalisation pipeline built from composable Function instances.

import java.util.List; import java.util.function.Function; public class PipelineDemo { static <T, R> List<R> mapList(List<T> items, Function<T, R> transform) { return items.stream().map(transform).toList(); } public static void main(String[] args) { Function<String, String> trim = String::trim; Function<String, String> lower = String::toLowerCase; Function<String, String> slug = s -> s.replace(" ", "-"); Function<String, String> toSlug = trim.andThen(lower).andThen(slug); List<String> titles = List.of(" Java Lambdas ", " Functional Style", "Clean Code "); List<String> slugs = mapList(titles, toSlug); slugs.forEach(System.out::println); // java-lambdas // functional-style // clean-code } }

Notice that each step is a named variable, making the pipeline self-documenting. You can reuse trim, lower, and slug independently in other pipelines — this is the composability payoff.

Summary

  • Function<T, R> transforms a value of type T into a value of type R via apply.
  • andThen(g) chains: this function first, then g (left-to-right).
  • compose(g) chains: g first, then this function (right-to-left).
  • BiFunction<T, U, R> accepts two arguments and returns one result.
  • UnaryOperator<T> and BinaryOperator<T> are shorthand when types are uniform.