Function Composition in Java 8: andThen, compose, and Predicate Chaining

  • Last Updated: August 12, 2026
  • By: javahandson
  • Series
img

Function Composition in Java 8: andThen, compose, and Predicate Chaining

Learn function composition in Java 8 with andThen, compose, and Predicate chaining. Simple examples, pipelines, common mistakes, and interview questions.

1. Introduction

You write small functions all the time. One trims a string. Another checks if it is empty. A third turns it into upper case. Each one does a single job well, and function composition in Java is what lets you snap these small jobs together into one.

But then a task comes along that needs two or three of these steps together. So what do you do? Many people just call one method, save the result, then call the next. That works, yet it gets messy fast.

Function composition in Java gives you a cleaner path. It lets you glue small functions into one bigger function. You build the chain once, then run it as a single unit.

Java 8 added this power right into the standard interfaces. Function has andThen and compose. Predicate has and, or, and negate. These tiny methods change how you stitch logic together.

In this guide we start from the ground up. First we look at what composition really means. Then we walk through each method with plain examples you can run yourself.

By the end, you will know when to chain functions and when to keep them apart. You will also dodge the small traps that catch people the first time.

Here is the plan for this article:

  • What function composition means, in plain words
  • How andThen runs functions front to back
  • How compose flips that order around
  • Chaining tests with Predicate.and, or, and negate
  • Composing with BiFunction and other function shapes
  • Real examples, common mistakes, and interview questions

You do not need any deep math background here. If you have written a lambda before, you are ready to dive in.

function composition in Java 8

2. What Is Function Composition?

Function composition means joining two functions into one. The output of the first becomes the input of the second. You end up with a single function that does both jobs.

Think of a kitchen line. One cook chops the onions. The next cook fries them. Together they turn a raw onion into a cooked one, in a fixed order.

2.1 A Simple Mental Model

Say you have two functions. One doubles a number. The other adds three. Composition lets you build a third function that does both, in a set order.

You do not have to run them one by one in your code. Instead you build the combined function once. Then you call it whenever you need that exact flow.

Function<Integer, Integer> doubleIt = x -> x * 2;
Function<Integer, Integer> addThree = x -> x + 3;
 
// Build one function that does both
Function<Integer, Integer> combined = doubleIt.andThen(addThree);
 
System.out.println(combined.apply(5)); // 13

Trace the flow. First we double 5 to get 10. Then we add 3 to get 13. The combined function ran both steps for us.

2.2 Why Not Just Call Them Separately?

Fair question. You could store each result in a variable and move on. For one or two steps that is fine and often clearer.

But composition shines when you pass logic around. You can hand a whole chain to another method as a single value. That is hard to do with loose, separate calls.

  • You want to reuse the same chain in many places.
  • The combined behaviour needs to travel as an argument.
  • Parts of the chain get chosen at run time.

So the goal is not to replace every simple call. The goal is to treat behaviour as data you can join and move around.

2.3 Functions as First-Class Values

Java 8 made functions feel like real values. You can store them in variables, pass them to methods, and return them. Composition builds on this idea.

When a function is a value, joining two of them is just another operation. You are not writing loops or branches. You are combining pieces, much like you add two numbers.

This shift is what makes the code read so cleanly. You describe what should happen, step by step, in one smooth line. The plumbing stays out of your way.

2.4 A Before and After

Let us make the gain concrete. Say you clean a name in two steps. Without composition, you juggle a temporary variable for each stage.

// Without composition
String trimmed = name.trim();
String result = trimmed.toUpperCase();
 
// With composition
Function<String, String> clean =
    ((Function<String, String>) String::trim)
        .andThen(String::toUpperCase);
String result2 = clean.apply(name);

For one call, the plain version is clearly fine. But the composed clean is now a value. You can reuse it, pass it, or drop it into a stream.

That reuse is the whole point. One name for a two-step job beats copying the steps around. As the logic grows, this pays off more and more.

3. The andThen Method

The andThen method is the most common way to compose functions. It runs the first function, then feeds the result into the second. So it flows left to right.

3.1 How andThen Works

When you call a.andThen(b), you get a new function. That new function first runs a on your input. Then it runs b on whatever a returned.

Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
 
Function<String, String> clean = trim.andThen(upper);
 
System.out.println(clean.apply("  hello  ")); // HELLO

First the trim runs and strips the spaces. Then the upper runs on the trimmed text. The result comes out clean and capitalised.

Notice the order matches how you read the code. Left runs first, right runs second. That reading order is a big reason people like andThen.

3.2 Chaining More Than Two

You are not stuck with just two functions. You can keep calling andThen to add more steps. Each new call tacks another function onto the end.

Function<Integer, Integer> addTwo = x -> x + 2;
Function<Integer, Integer> timesTen = x -> x * 10;
Function<Integer, Integer> minusFive = x -> x - 5;
 
Function<Integer, Integer> pipeline =
    addTwo.andThen(timesTen).andThen(minusFive);
 
System.out.println(pipeline.apply(3)); // 45

Walk it through slowly. We add 2 to 3 and get 5. Then we times 10 for 50. Then we subtract 5 for a final 45.

Each function passes its result to the next. So the data flows down the chain, step by step. This reads like a little assembly line.

3.3 The Types Must Line Up

There is one rule to keep in mind. The output type of the first function must match the input type of the second. Otherwise the code will not compile.

In our string example, both steps take and return a String. So they slot together with no fuss. But mix in a type mismatch and the compiler stops you.

This type safety is a good thing. It catches a broken chain before you ever run it. You find out at compile time, not in production.

💡 Interview Insight
The andThen method reads in the same order it runs. So a.andThen(b) runs a first, then b. Interviewers often follow up by asking how compose differs. Keep that contrast ready, because the two are mirror images of each other.

4. The compose Method

The compose method also joins two functions. But it runs them in the opposite order from andThen. The function you pass in runs first.

4.1 How compose Works

When you call a.compose(b), the b function runs first. Then a runs on the result of b. So it flows right to left.

Function<Integer, Integer> doubleIt = x -> x * 2;
Function<Integer, Integer> addThree = x -> x + 3;
 
Function<Integer, Integer> combined = doubleIt.compose(addThree);
 
System.out.println(combined.apply(5)); // 16

Here addThree runs first on 5, giving 8. Then doubleIt runs and gives 16. The passed-in function led the way.

Compare this with the andThen version from earlier. Same two functions, same input, yet a different answer. The order alone changed the result.

4.2 andThen vs compose Side by Side

The two methods are mirror images. Picking the right one comes down to which function you want to run first. This table lays it out.

Call Runs First Runs Second
a.andThen(b) a b
a.compose(b) b a

So both methods build a chain of two. The only question is which end starts. Once that clicks, you will reach for the right one every time.

4.3 When compose Reads Better

Most of the time andThen feels natural. It matches the left-to-right way we read code. But sometimes compose maps better to how we speak about a task.

Say you want to “validate, then format” a value. You might already hold a format function and wrap validation around it. In that case compose can express the idea without reordering your variables.

Still, if you are ever unsure, stick with andThen. It trips people up less often. Save compose for the cases where it truly reads cleaner.

💡 Interview Insight
A classic interview trick is to give the same two functions to both andThen and compose. Then they ask for the output of each. The answers differ because the run order flips. Always trace the input through each step by hand before you answer.

5. Composing Predicates

A Predicate is a function that returns true or false. Java 8 lets you combine predicates too. You get and, or, and negate to build richer tests.

5.1 The Predicate.and Method

The and method joins two predicates. The result is true only when both hold. It works just like the && operator, but as a reusable value.

Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;
 
Predicate<Integer> positiveAndEven = isPositive.and(isEven);
 
System.out.println(positiveAndEven.test(4));  // true
System.out.println(positiveAndEven.test(-4)); // false
System.out.println(positiveAndEven.test(3));  // false

The number 4 passes both tests, so we get true. But -4 fails the positive check. And 3 fails the even check, so both give false.

5.2 The Predicate.or Method

The or method also joins two predicates. This time the result is true when at least one holds. It mirrors the || operator.

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isShort = s -> s.length() < 3;
 
Predicate<String> emptyOrShort = isEmpty.or(isShort);
 
System.out.println(emptyOrShort.test(""));    // true
System.out.println(emptyOrShort.test("hi"));  // true
System.out.println(emptyOrShort.test("hello")); // false

An empty string passes the first test. A two-letter string passes the second. But a five-letter string fails both, so it returns false.

5.3 The Predicate.negate Method

The negate method flips a predicate. A true becomes false, and a false becomes true. It is the same as the ! operator.

Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isNotPositive = isPositive.negate();
 
System.out.println(isNotPositive.test(5));  // false
System.out.println(isNotPositive.test(-5)); // true
System.out.println(isNotPositive.test(0));  // true

So 5 is positive, and the flip makes it false. Both -5 and 0 are not positive, so they turn true. The negate simply inverts the answer.

5.4 Chaining Them Together

The real power shows when you mix all three. You can build a full test from small, named parts. Each part stays easy to read on its own.

Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isBig = n -> n > 100;
 
Predicate<Integer> rule =
    isPositive.and(isEven).or(isBig);
 
System.out.println(rule.test(4));   // true  (positive and even)
System.out.println(rule.test(101)); // true  (big)
System.out.println(rule.test(3));   // false

The number 4 is positive and even, so it passes. The number 101 is big, so the or clause saves it. But 3 fails every branch.

💡 Interview Insight
Predicate methods follow the same precedence as normal boolean logic, but only through the order you chain them. So isPositive.and(isEven).or(isBig) reads left to right. When mixing and with or, wrap the parts in named predicates to keep the intent clear. Interviewers like to see you avoid a tangled one-liner.

6. Composing Other Function Types

Composition is not just for Function and Predicate. Several other interfaces support it too. The idea stays the same across all of them.

6.1 BiFunction With andThen

A BiFunction takes two inputs and returns one result. It also has an andThen method. You can run a normal Function on its output.

BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
Function<Integer, String> label = sum -> "Total: " + sum;
 
BiFunction<Integer, Integer, String> addAndLabel =
    add.andThen(label);
 
System.out.println(addAndLabel.apply(3, 4)); // Total: 7

First the BiFunction adds 3 and 4 to get 7. Then the label function turns that into text. Note that BiFunction has no compose, only andThen.

6.2 Consumer With andThen

A Consumer takes a value and returns nothing. Its andThen runs two consumers in order on the same input. This is handy for side tasks like logging.

Consumer<String> print = s -> System.out.println("Value: " + s);
Consumer<String> log = s -> System.out.println("Logged: " + s);
 
Consumer<String> both = print.andThen(log);
 
both.accept("data");
// Value: data
// Logged: data

Both consumers get the same input, one after the other. So you run two side effects with a single call. The order follows the chain you built.

6.3 A Quick Reference Table

Not every functional interface offers the same methods. This table shows which composition methods you get on the common ones.

Interface Composition Methods
Function andThen, compose
BiFunction andThen only
Predicate and, or, negate
Consumer andThen only
BiConsumer andThen only

So keep this in mind when you plan a chain. Function is the most flexible. The others give you a smaller, focused set.

6.4 UnaryOperator and Function.identity

A UnaryOperator is a Function where the input and output share a type. It inherits andThen and compose from Function. So it composes exactly the same way.

There is also a handy no-op called Function.identity. It just returns its input unchanged. This helps when you fold a list of functions into one.

List<Function<Integer, Integer>> steps = List.of(
    x -> x + 1,
    x -> x * 2,
    x -> x - 3);
 
Function<Integer, Integer> all =
    steps.stream().reduce(Function.identity(), Function::andThen);
 
System.out.println(all.apply(5)); // ((5+1)*2)-3 = 9

Here we start with identity as a safe seed. Then reduce folds each step in with andThen. The result is one function built from a list.

This trick is neat when the steps are dynamic. Maybe they come from config or user choice. You still end up with a single, tidy function.

7. Returning Composed Functions

So far we built chains inline. But you can also return a composed function from a method. This lets you package a whole pipeline behind a clean name.

7.1 A Factory Method

A method can build a chain and hand it back. The caller gets a ready function without knowing the parts. This hides the plumbing behind one call.

static Function<String, String> makeCleaner() {
    return ((Function<String, String>) String::trim)
        .andThen(String::toLowerCase);
}
 
Function<String, String> cleaner = makeCleaner();
System.out.println(cleaner.apply("  HELLO ")); // hello

The method builds the chain and returns it. The caller just applies the result. So the details stay in one tidy place.

7.2 Parameterised Pipelines

You can go further and pass in options. The method then builds a chain that fits those options. This gives you flexible, reusable factories.

static Function<Integer, Integer> scaler(int factor) {
    return x -> x * factor;
}
 
Function<Integer, Integer> pipeline =
    scaler(3).andThen(x -> x + 1);
 
System.out.println(pipeline.apply(4)); // 13

The scaler method builds a function from a value. Then we chain it like any other. So one small factory feeds many different pipelines.

8. A Practical Walkthrough

Theory sinks in better with real code. Let us build a small text-cleaning pipeline. We will compose several steps into one reusable function.

8.1 The Problem

Imagine user input coming from a form. It may have extra spaces, mixed case, and stray tags. We want one function that cleans it all up.

Rather than one giant method, we build small parts. Each part does a single, clear job. Then we compose them into the final cleaner.

8.2 Building the Parts

First we define each step as its own function. Every step takes a String and returns a String. That shared shape lets them chain freely.

Function<String, String> trim = String::trim;
Function<String, String> lower = String::toLowerCase;
Function<String, String> stripTags =
    s -> s.replaceAll("<[^>]+>", "");

Each function is tiny and easy to test. You can check them one at a time. That makes bugs simple to track down later.

8.3 Composing the Pipeline

Now we chain the parts with andThen. The order matters, so we strip tags first, then trim, then lower. Read it top to bottom like a recipe.

Function<String, String> cleanInput =
    stripTags
        .andThen(trim)
        .andThen(lower);
 
String raw = "  <b>Hello WORLD</b>  ";
System.out.println(cleanInput.apply(raw)); // hello world

The tags go first, leaving spaces around the words. Then the trim removes those spaces. Finally the lower drops it all to small letters.

8.4 Reusing It Everywhere

The cleanInput function is now a value you can pass around. Send it to a stream, hand it to a method, or store it in a field. It carries the whole pipeline with it.

List<String> inputs = List.of("  <i>ONE</i> ", " Two  ");
 
inputs.stream()
      .map(cleanInput)
      .forEach(System.out::println);
// one
// two

Here we plug the pipeline straight into a stream map. Every item runs through the same clean chain. That is composition paying off in real code.

9. When to Use Function Composition

Composition is a sharp tool, but not every job needs it. It helps in some spots and only adds noise in others. Let us weigh both sides.

9.1 Good Fits

Reach for composition when you build reusable pipelines. These cases suit it well:

  • You apply the same series of steps in many places.
  • Behaviour needs to travel as an argument to another method.
  • Functions plug into a stream map or filter.
  • Small, testable parts should join into one flow.

9.2 Poor Fits

Skip composition when it hurts clarity. In these cases plain code reads better:

  • You run a step just once, in one place.
  • The chain grows so long it is hard to follow.
  • Each step needs its own error handling in between.

So think about reuse and readability first. If a chain makes the code harder to grasp, break it apart. Clear code always wins over clever code.

9.3 Composition vs Stream Pipelines

People sometimes ask how this differs from a stream. Both feel like a pipeline of steps. But they solve slightly different problems.

A stream runs steps over a collection of items. Composition builds one function you can apply to a single value. You often use the two together.

Function<String, String> clean =
    ((Function<String, String>) String::trim)
        .andThen(String::toLowerCase);
 
List<String> out = List.of(" A ", " B ").stream()
    .map(clean)
    .toList();
// [a, b]

So the composed function becomes the body of the map. The stream handles the looping. Composition handles the per-item logic.

This split keeps each part focused. Your function stays testable on its own. Then the stream just applies it across the data.

9.4 A Word on Readability

Composition can make code elegant or cryptic. The line between them is naming. Give each small function a clear name, and the chain reads like plain English.

Avoid stuffing raw lambdas into a long chain. A row of anonymous arrows is hard to scan. Named parts turn that same chain into a readable story.

10. Common Mistakes and Pitfalls

A few traps catch people when they first use composition. Knowing them early saves you some head-scratching.

10.1 Mixing Up andThen and compose

This is the top mistake. People assume both run left to right. But compose runs the passed function first, which flips the order.

Always trace the input by hand when you are unsure. Walk it through each step and note the value. That habit catches the wrong-order bug fast.

10.2 Mismatched Types in a Chain

Every link in the chain must fit the next. If one function returns a type the next cannot take, it will not compile. This bites when steps return different types.

The fix is to check the input and output types at each step. Line them up before you chain. The compiler helps, but reading the types yourself saves time.

10.3 Forgetting Predicate Precedence

When you mix and with or, the chain order decides the meaning. So isBig.or(isPositive).and(isEven) is not the same as the reverse. It is easy to build the wrong logic.

Break complex tests into named predicates. Then combine those names in a clear final step. This makes the intent obvious and dodges silent bugs.

10.4 Overusing Composition

Some people chain everything once they learn how. But a five-step chain of raw lambdas is hard to read. Composition should aid clarity, not hurt it.

// Hard to read
Function<Integer, Integer> f =
    ((Function<Integer, Integer>) x -> x + 1)
        .andThen(x -> x * 2)
        .andThen(x -> x - 3)
        .andThen(x -> x * x);

This works, yet it is a wall of arrows. Name each step and the chain turns friendly. When in doubt, favour the reader over the clever line.

10.5 Null Results in a Chain

A chain passes each result straight to the next step. If one step returns null, the next may throw. Nothing in composition guards against that.

So handle null inside the step that might produce it. Return a safe default, or filter nulls before the chain. Do not assume every step gives a clean value.

11. Common Interview Angles

Function composition pops up often in Java interviews. It touches lambdas, functional interfaces, and clean design. Here are the angles that come up most.

11.1 Explain andThen vs compose

This is the classic opener. The short answer is the run order. andThen runs the caller first, while compose runs the argument first.

Back it up with a quick trace. Show the same input giving two answers. That proves you understand it, not just memorised it.

11.2 Predicate Combination

They may ask how to build a complex filter. Talk about and, or, and negate. Show how small predicates join into one.

Add the precedence point too. Explain that the chain order sets the logic. That detail shows real depth.

11.3 Which Interfaces Support It

A sharp follow-up asks about other types. Mention that BiFunction and Consumer have andThen but no compose. Predicate has its own trio instead.

Knowing these limits shows hands-on use. It tells the interviewer you have chained real code, not just read about it.

11.4 A Practical Use Case

Finally they may ask where you would use it. Give a concrete case, like a text-cleaning pipeline or a validation chain. Tie it to a stream map for extra points.

That kind of answer lands well. It shows you know the tool and the moment to reach for it. That beats reciting the method names alone.

12. Interview Questions

Q: What is function composition in Java?

A: Function composition joins two functions into one, where the output of the first becomes the input of the second. Java 8 supports it through Function.andThen and Function.compose, letting you build a reusable pipeline you can apply as a single value.

Q: What is the difference between andThen and compose?

A: They run in opposite orders. In a.andThen(b) the caller a runs first, then b. In a.compose(b) the argument b runs first, then a. The same two functions can give different results depending on which you use.

Q: How do Predicate.and, or, and negate work?

A: The and method is true only when both predicates hold, or is true when at least one holds, and negate flips the result. They mirror the &&, ||, and ! operators but as reusable Predicate values you can chain.

Q: Does BiFunction support compose?

A: No. BiFunction only has andThen, so you can run a normal Function on its result but cannot prepend a step with compose. Function is the only common interface that offers both andThen and compose.

Q: Do the types need to match when composing functions?

A: Yes. In andThen, the output type of the first function must match the input type of the second, or the code will not compile. This type safety catches a broken chain at compile time instead of at run time.

Q: When should I use function composition instead of separate calls?

A: Reach for it when you reuse the same series of steps, pass behaviour as an argument, or plug logic into a stream map or filter. For a one-off step in one place, plain separate calls are usually clearer.

13. Conclusion

Function composition in Java lets you glue small functions into one. The andThen method runs left to right, and compose flips that around. Both turn separate steps into a single, reusable flow.

Predicates get their own set of tools. With and, or, and negate you build rich tests from simple parts. Each piece stays easy to read and easy to test.

The real win is treating behaviour as a value. You can name a chain, pass it around, and drop it into a stream. That is a clean, modern way to write Java.

Just keep an eye on clarity. A short, well-named chain is a joy to read. A long wall of raw lambdas is not, so break it up when it grows.

So the next time you stack a few small functions, think about composing them. Build the parts, join them once, and let the chain do the work. Your future self will thank you.

Open your editor and try the samples above. Swap andThen for compose and watch the answer change. That hands-on play is what makes the ideas stick.

Further Reading

Leave a Comment