CodeGym /Courses /JAVA 25 SELF /Function composition: compose, andThen

Function composition: compose, andThen

JAVA 25 SELF
Level 49 , Lesson 1
Available

1. The concept of function composition

A bit of theory (but not too dry)

In mathematics, function composition is when the result of one function becomes the input to another. If we have functions f and g, the composition g(f(x)) means: first apply f to x, then feed the result into g.

In programming, the idea is the same: we want to assemble complex transformations from simple ones, instead of writing one giant function for everything. This makes the code flexible, reusable, and readable.

Imagine a pastry assembly line: first the dough (f), then the cream (g), then the sprinkles (h). The whole process is h(g(f(ingredients))).

Why is composition important?

  • Composition lets you build a program out of small function “blocks”.
  • It’s easier to reuse: a single “block” can be plugged into different places without duplication.
  • Flexibility: to change one stage, replace the corresponding function — the rest stays untouched.
  • Readability and testability: small functions are easier to read, verify, and maintain.

2. Methods compose and andThen in the Function interface

Function interface: a quick refresher

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

    // Methods for composition:
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
}
  • compose: first the function you pass to compose runs, and then the current one.
  • andThen: first the current function runs, and then the one passed to andThen.

Visual diagram

// Suppose there are two functions:
Function<String, Integer> parse = s -> Integer.parseInt(s);
Function<Integer, Integer> square = x -> x * x;

// compose: square.compose(parse) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// andThen: parse.andThen(square) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// But order matters if the types differ!

Example: convert a string to a number, then square it

import java.util.function.Function;

public class ComposeAndThenDemo {
    public static void main(String[] args) {
        // Function: converts a string to a number
        Function<String, Integer> parse = s -> Integer.parseInt(s);

        // Function: squares a number
        Function<Integer, Integer> square = x -> x * x;

        // Combine: parse first, then square
        Function<String, Integer> parseThenSquare = parse.andThen(square);

        System.out.println(parseThenSquare.apply("7")); // 49

        // What if we swap them?
        // square.compose(parse) — the same result (for these functions)
        Function<String, Integer> squareOfParsed = square.compose(parse);

        System.out.println(squareOfParsed.apply("8")); // 64
    }
}

When does order matter?

If the function types don’t match, order becomes critical. For example:

Function<String, String> addPrefix = s -> "User: " + s;
Function<String, Integer> length = s -> s.length();

Function<String, Integer> composed = addPrefix.andThen(length);
System.out.println(composed.apply("Alice")); // "User: Alice" -> 11

// But this way:
// length.andThen(addPrefix) — compilation error!
// length returns Integer, but addPrefix accepts String.

Table: difference between compose and andThen

f
g
f.compose(g)
f.andThen(g)
T → R
V → T
V → R: f(g(x))
T → V: g(f(x))

3. Composing predicates and other interfaces

Predicate<T>: and, or, negate

The functional interface Predicate<T> is a function that returns boolean. To combine predicates there are special methods:

  • and: logical AND (&&)
  • or: logical OR (||)
  • negate: logical NOT (!)

Example: complex filter conditions

Suppose we have a user class:

public class User {
    String name;
    int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Now create several predicates:

import java.util.function.Predicate;

Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");

// Combine: adult and name starting with "A"
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);

// Adult or name starting with "A"
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);

// Not an adult
Predicate<User> notAdult = isAdult.negate();

Now we can use these predicates in filtering, for example with the Stream API:

import java.util.List;
import java.util.stream.Collectors;

List<User> users = List.of(
    new User("Alice", 20),
    new User("Bob", 17),
    new User("Anna", 15),
    new User("Mike", 22)
);

List<User> filtered = users.stream()
    .filter(adultAndA)
    .collect(Collectors.toList());

// Only Alice will end up in filtered (adult and name starting with "A")

Composing Consumer, Function, Supplier

  • Consumer<T>: the andThen method — lets you perform two operations in sequence.
  • Function<T, R>: composition was covered above.
  • Supplier<T>: not composed directly, but can be used inside other functions.

Example: Consumer<T>.andThen

import java.util.function.Consumer;

Consumer<String> print = s -> System.out.println("Received: " + s);
Consumer<String> printUpper = s -> System.out.println("Uppercase: " + s.toUpperCase());

Consumer<String> combined = print.andThen(printUpper);

combined.accept("hello");
// Output:
// Received: hello
// Uppercase: HELLO

4. Practice: chains of transformations and filtering

Task 1: Build a Function transformation chain

Suppose our application stores users as strings "Name,Age", for example "Alice,20". We need to:

  • Convert the string to a User object
  • Get the age
  • Check whether the user is an adult
import java.util.function.Function;
import java.util.function.Predicate;

Function<String, User> stringToUser = str -> {
    String[] parts = str.split(",");
    return new User(parts[0], Integer.parseInt(parts[1]));
};
Function<User, Integer> getAge = user -> user.age;
Predicate<Integer> isAdultAge = age -> age >= 18;

// Combine: string -> User -> age -> predicate
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);

String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("Age: " + age); // 19

System.out.println("Adult? " + isAdultAge.test(age)); // true

Task 2: Combine multiple Predicate for filtering

Suppose we need to select users older than 18 whose names start with "A" or "M".

Predicate<User> isAdult = user -> user.age > 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
Predicate<User> nameStartsWithM = user -> user.name.startsWith("M");

Predicate<User> filter = isAdult.and(nameStartsWithA.or(nameStartsWithM));

List<User> filtered = users.stream()
    .filter(filter)
    .collect(Collectors.toList());

Task 3: Multi-stage Function transformation

Goal: take a string, trim whitespace, convert to upper case, add the prefix "USER: ".

Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;

// Build the chain
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);

System.out.println(pipeline.apply("   john   ")); // USER: VASYA

5. Common mistakes when composing functions

Error #1: compose/andThen order is mixed up.
Beginners often confuse what runs first and what runs second. Remember: f.compose(g) — first g, then f; f.andThen(g) — first f, then g.

Error #2: Type mismatch.
If the result type of one function doesn’t match the parameter type of another, the compiler won’t let you compose them. For example, you can’t do Function<Integer, String>.andThen(Function<Double, Boolean>).

Error #3: Overly complex chains.
Sometimes you want to cram all business logic into a single chain and get “spaghetti”. Break it up into small functions and give them clear names.

Error #4: Side effects inside functions.
It’s better to keep functions and predicates “pure” (without side effects), otherwise composition becomes risky and unpredictable.

1
Task
JAVA 25 SELF, level 49, lesson 1
Locked
Standardizing Logins for the Secret Society 🕵️‍♂️
Standardizing Logins for the Secret Society 🕵️‍♂️
1
Task
JAVA 25 SELF, level 49, lesson 1
Locked
Magic Number Transformations at the School of Wizards 🧙‍♀️
Magic Number Transformations at the School of Wizards 🧙‍♀️
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION