Java Streams are built on the foundation of functional interfaces—special interfaces with a single abstract method—that enable concise and declarative data processing pipelines. The most commonly used functional interfaces in streams are:
These interfaces empower streams to express complex transformations, filters, and side effects with minimal boilerplate.
Streams leverage these functional interfaces to build pipelines where each step declares what should be done rather than how. For example:
filter(Predicate<T> predicate)
only lets through elements matching a condition.map(Function<T, R> mapper)
transforms elements from one form to another.forEach(Consumer<T> action)
performs side effects like printing.Using lambdas or method references makes this code clean, readable, and highly expressive.
Predicate
import java.util.List;
public class PredicateExample {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
// Filter names that start with 'A' using a lambda Predicate
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
}
}
Output:
Alice
Here, the lambda name -> name.startsWith("A")
implements Predicate<String>
to test each element.
Function
import java.util.List;
public class FunctionExample {
public static void main(String[] args) {
List<String> words = List.of("apple", "banana", "cherry");
// Convert each word to uppercase using a method reference Function
words.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
}
}
Output:
APPLE
BANANA
CHERRY
String::toUpperCase
is a concise method reference that matches Function<String, String>
.
Consumer
import java.util.List;
public class ConsumerExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4);
// Print each number with a prefix using a custom Consumer lambda
numbers.stream()
.forEach(n -> System.out.println("Number: " + n));
}
}
Output:
Number: 1
Number: 2
Number: 3
Number: 4
The lambda n -> System.out.println("Number: " + n)
defines a Consumer<Integer>
for side effects.
By combining functional interfaces with streams:
Predicate
, Function
, and Consumer
are the building blocks of common stream operations such as filtering, mapping, and consuming.This synergy between Streams and functional interfaces is key to mastering modern Java data processing patterns.
Function composition is a powerful technique that enhances the clarity, reusability, and modularity of stream pipelines. By combining smaller functions into larger ones, you can build complex transformations or filters in a clean, declarative manner. Java’s functional interfaces like Function
and Predicate
provide built-in methods to facilitate this composition:
Function.andThen()
: Chains two functions, applying the first, then the second.Function.compose()
: Chains two functions, applying the second, then the first.Predicate.and()
/ Predicate.or()
: Combines multiple boolean conditions logically.Using these methods allows you to write reusable, composable logic blocks that can be passed directly to stream operations like map()
or filter()
.
andThen
: Applies the current function, then passes the result to the next function.
compose
: Applies the argument function first, then the current function.
This distinction is important when chaining multiple transformations, enabling flexible ordering.
Function.andThen()
Suppose you want to process a list of names by trimming whitespace and then converting to uppercase. Instead of writing a single lambda, compose two reusable functions:
import java.util.List;
import java.util.function.Function;
public class FunctionCompositionExample {
public static void main(String[] args) {
List<String> names = List.of(" Alice ", " Bob", "Charlie ");
Function<String, String> trim = String::trim;
Function<String, String> toUpperCase = String::toUpperCase;
// Compose functions: first trim, then convert to uppercase
Function<String, String> trimAndUpperCase = trim.andThen(toUpperCase);
names.stream()
.map(trimAndUpperCase)
.forEach(System.out::println);
}
}
Output:
ALICE
BOB
CHARLIE
By composing trim
and toUpperCase
, the code becomes modular and easy to maintain.
Predicate.and()
and Predicate.or()
Imagine filtering a list of integers to include only those that are positive and even, or greater than 50:
import java.util.List;
import java.util.function.Predicate;
public class PredicateCompositionExample {
public static void main(String[] args) {
List<Integer> numbers = List.of(10, 25, 42, 55, 60, -4, 0);
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isGreaterThan50 = n -> n > 50;
// Compose predicates: (positive AND even) OR greater than 50
Predicate<Integer> complexCondition = isPositive.and(isEven).or(isGreaterThan50);
numbers.stream()
.filter(complexCondition)
.forEach(System.out::println);
}
}
Output:
10
42
55
60
This example cleanly expresses a complex conditional filter by composing simple predicates.
Function composition using Function.andThen()
, compose()
, and Predicate.and()/or()
empowers you to build elegant, reusable logic blocks for stream pipelines. This approach leads to cleaner, more declarative code when performing chained transformations or complex filtering — essential for writing maintainable and expressive Java stream-based data processing.
Currying and partial application are foundational concepts in functional programming that enable building flexible, reusable functions by breaking down functions with multiple arguments into chains of single-argument functions. These techniques can greatly simplify and modularize logic when working with Java Streams.
Currying transforms a function that takes multiple arguments into a sequence of functions each taking a single argument. For example, a function of two arguments (A, B) -> R
becomes A -> (B -> R)
— a function that returns another function.
In Java, currying is often implemented with lambdas that return other lambdas.
Partial application fixes a few arguments of a multi-argument function, producing a new function of fewer arguments. For example, partially applying the first argument of a two-argument function f(A, B)
yields a single-argument function g(B)
.
Currying and partial application help build reusable and parameterized stream operations, such as:
This leads to concise, composable pipelines without repetitive boilerplate.
Suppose you want to create a filter predicate to check if strings have length greater than a dynamic threshold. Currying helps create a reusable predicate factory:
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
public class CurryingExample {
// Curried function: int -> Predicate<String>
static Function<Integer, Predicate<String>> lengthGreaterThan =
length -> str -> str.length() > length;
public static void main(String[] args) {
List<String> words = List.of("apple", "pear", "banana", "kiwi", "pineapple");
// Partially apply length = 4
Predicate<String> longerThan4 = lengthGreaterThan.apply(4);
words.stream()
.filter(longerThan4)
.forEach(System.out::println);
}
}
Output:
apple
banana
pineapple
Here, lengthGreaterThan
is a curried function that produces a predicate configured by a threshold. This keeps filtering logic reusable and clean.
Imagine mapping integers to strings with a customizable prefix. You can partially apply the prefix to create reusable mappers:
import java.util.List;
import java.util.function.Function;
public class PartialApplicationExample {
// Function that takes prefix and returns function from Integer to String
static Function<String, Function<Integer, String>> prefixer =
prefix -> number -> prefix + number;
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Partially apply prefix "Item-"
Function<Integer, String> itemNamer = prefixer.apply("Item-");
numbers.stream()
.map(itemNamer)
.forEach(System.out::println);
}
}
Output:
Item-1
Item-2
Item-3
Item-4
Item-5
This pattern can generate various mapping functions dynamically based on context.
Currying and partial application bring powerful abstraction to stream processing by enabling you to build parameterized, reusable functions that cleanly integrate with map()
, filter()
, and other stream operations. By structuring lambdas as chains of single-argument functions, you achieve more expressive and maintainable pipelines that adapt flexibly to different use cases.