Combining, Chaining, and Composing Lambdas

1. Why Combine Lambdas?

Instead of putting a lot of logic into one lambda, you can create small lambdas and combine them.

For example:

Predicate<String> nonNull = s -> s != null;
Predicate<String> nonEmpty = s -> !s.isEmpty();
Predicate<String> shortString = s -> s.length() < 5;

Then combine them:

Predicate<String> valid =
    nonNull.and(nonEmpty).and(shortString);

This makes the logic easier to understand and reuse.


2. Combining Predicate

A Predicate<T>:

T → boolean

For example:

Predicate<Integer> isEven = n -> n % 2 == 0;

and()

Both conditions must be true.

Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive = n -> n > 0;

Predicate<Integer> valid = isEven.and(isPositive);

Now:

valid.test(4);  // true
valid.test(-4); // false
valid.test(3);  // false

Think:

isEven AND isPositive

or()

At least one condition must be true.

Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositive = n -> n > 0;

Predicate<Integer> valid = isEven.or(isPositive);
valid.test(4);  // true
valid.test(-4); // true  (even)
valid.test(3);  // true  (positive)

Think:

isEven OR isPositive

negate()

Reverses the result.

Predicate<Integer> isEven = n -> n % 2 == 0;

Predicate<Integer> isOdd = isEven.negate();

Now:

isOdd.test(3); // true
isOdd.test(4); // false

Think:

negate(true)  → false
negate(false) → true

3. Predicate.isEqual()

Predicate.isEqual(value) creates a predicate that checks equality.

Predicate<String> isJava =
    Predicate.isEqual("Java");

Now:

isJava.test("Java"); // true
isJava.test("Python"); // false

Equivalent to:

s -> s.equals("Java")

4. Predicate.not()

Predicate.not() creates the opposite of a predicate.

Predicate<String> isEmpty = String::isEmpty;

Predicate<String> isNotEmpty = Predicate.not(isEmpty);

Now:

isNotEmpty.test("");      // false
isNotEmpty.test("Hello"); // true

Equivalent idea:

isNotEmpty = NOT isEmpty

5. Chaining Consumer with andThen()

A Consumer<T>:

T → nothing

Example:

Consumer<String> print =
    s -> System.out.println(s);

Consumer<String> printLength =
    s -> System.out.println(s.length());

Combine them:

Consumer<String> both =
    print.andThen(printLength);

Now:

both.accept("Hello");

Output:

Hello
5

The order is:

print
  ↓
printLength

So:

a.andThen(b)

means:

Do a, then do b.


6. Function.andThen()

A Function<T, R> transforms one value into another:

T → R

Example:

Function<String, Integer> length =
    String::length;

Function<Integer, Boolean> even =
    n -> n % 2 == 0;

Combine them:

Function<String, Boolean> evenLength =
    length.andThen(even);

Now:

System.out.println(evenLength.apply("Hello"));

What happens?

"Hello"
   ↓
length
   ↓
5
   ↓
even
   ↓
false

So:

length.andThen(even)

means:

even(length(x))

7. Function.compose()

compose() also combines functions, but the order can initially look confusing.

Function<String, Integer> length =
    String::length;

Function<Integer, Boolean> even =
    n -> n % 2 == 0;

We can write:

Function<String, Boolean> evenLength =
    even.compose(length);

This does:

String
  ↓
length
  ↓
Integer
  ↓
even
  ↓
Boolean

So:

even.compose(length)

means:

even(length(x))

The important rule

f.andThen(g)

means:

f → g

while:

g.compose(f)

also means:

f → g

Therefore:

length.andThen(even)

and:

even.compose(length)

do the same thing.


8. andThen() vs compose()

This is worth remembering:

f.andThen(g)
     ↓
    f first, g second
g.compose(f)
       ↓
    f first, g second

Example:

Function<Integer, Integer> multiplyBy2 =
    x -> x * 2;

Function<Integer, Integer> add10 =
    x -> x + 10;

andThen()

Function<Integer, Integer> result =
    multiplyBy2.andThen(add10);

For 5:

5
↓
×2
↓
10
↓
+10
↓
20

compose()

Function<Integer, Integer> result =
    add10.compose(multiplyBy2);

Same result:

5 → ×2 → +10 → 20

9. Function.identity()

identity() simply returns whatever it receives.

Function<String, String> identity =
    Function.identity();

Then:

System.out.println(identity.apply("Hello"));

Output:

Hello

Conceptually:

x -> x

So:

Input → Same Input

Example:

Function<Integer, Integer> identity =
    Function.identity();

identity.apply(10); // 10

10. A Practical Example

Suppose we want to check whether a string:

  1. Is not empty

  2. Has less than 10 characters

  3. Contains "Java"

We can create separate predicates:

Predicate<String> notEmpty =
    Predicate.notisEmpty;

Predicate<String> shortString =
    s -> s.length() < 10;

Predicate<String> containsJava =
    s -> s.contains("Java");

Combine them:

Predicate<String> valid =
    notEmpty
        .and(shortString)
        .and(containsJava);

Now:

valid.test("Java");       // true
valid.test("Java Rocks"); // false
valid.test("");           // false

Instead of one large condition, we built the final logic from small pieces.


Quick Revision

Interface Method Purpose
Predicate and() Both conditions must be true
Predicate or() At least one must be true
Predicate negate() Reverses the result
Predicate isEqual() Creates an equality predicate
Predicate not() Creates the opposite predicate
Consumer andThen() Execute consumers in sequence
Function andThen() First function → second function
Function compose() Given function → current function
Function identity() Returns input unchanged

The main idea

Predicate → combine conditions
Consumer  → combine actions
Function  → combine transformations

The most important ones to remember are:

predicate1.and(predicate2)
predicate1.or(predicate2)
predicate.negate()

consumer1.andThen(consumer2)

function1.andThen(function2)
function2.compose(function1)

Function.identity()