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 dob.
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:
-
Is not empty
-
Has less than 10 characters
-
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()