Java's Streams API, introduced in Java 8, revolutionized how developers interact with collections by enabling declarative, functional-style data processing. Instead of writing verbose for
-loops or mutating data imperatively, you can now express operations like filtering, transforming, sorting, and aggregating in a clean, composable way.
Traditional loops operate eagerly, mix logic and iteration, and often mutate intermediate state. In contrast, the Streams API:
filter
, map
, and collect
are chained fluently.Let’s explore the most common operations with examples:
import java.util.List;
import java.util.stream.Collectors;
public class StreamBasics {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(filtered); // [ALICE, CHARLIE, DAVID]
}
}
filter
removes elements based on a condition.map
transforms each element (in this case, to uppercase).collect
gathers the result into a new list (immutably).List<Integer> numbers = List.of(5, 3, 4, 3, 1, 2);
List<Integer> sorted = numbers.stream()
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(sorted); // [1, 2, 3, 4, 5]
distinct
removes duplicates.sorted
orders the elements naturally or via a comparator.import java.util.Map;
import java.util.stream.Collectors;
List<String> animals = List.of("cat", "cow", "dog", "dolphin");
Map<Character, List<String>> grouped = animals.stream()
.collect(Collectors.groupingBy(name -> name.charAt(0)));
System.out.println(grouped); // {c=[cat, cow], d=[dog, dolphin]}
groupingBy
groups elements by a classifier function.Map<Boolean, List<String>> partitioned = animals.stream()
.collect(Collectors.partitioningBy(name -> name.length() > 3));
System.out.println(partitioned); // {false=[cat, cow, dog], true=[dolphin]}
partitioningBy
splits the stream into two groups based on a predicate.collect
, forEach
, count
)..parallelStream()
when needed.The Streams API enables expressive, functional-style manipulation of Java collections. By chaining operations like map
, filter
, and collect
, developers can write cleaner, safer, and more maintainable code. Whether filtering lists, sorting elements, or grouping by criteria, streams provide a declarative approach that fits naturally with modern functional programming techniques in Java.
Comparator
, Runnable
)While Java 8 introduced the java.util.function
package with core functional interfaces like Function
, Predicate
, and Consumer
, several long-standing interfaces in Java’s standard library are also functional by design. These interfaces typically define a single abstract method, making them ideal targets for lambda expressions and method references.
Let’s explore some widely used functional interfaces already present in common Java APIs.
Comparator<T>
Functional SortingThe Comparator<T>
interface is used to define custom ordering logic for objects. Its single abstract method is:
int compare(T o1, T o2);
Example: Sorting a list of strings by length
import java.util.*;
public class ComparatorExample {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
names.stream()
.sorted(Comparator.comparingInt(String::length))
.forEach(System.out::println); // Bob, Alice, Charlie
}
}
The use of Comparator.comparingInt
with a method reference makes the sort logic declarative and functional.
Runnable
Deferred ExecutionRunnable
represents a task to run, typically in a separate thread. Its abstract method:
void run();
Example: Executing a task in a new thread
public class RunnableExample {
public static void main(String[] args) {
Runnable task = () -> System.out.println("Running task...");
new Thread(task).start();
}
}
Using a lambda for Runnable
avoids boilerplate like anonymous classes, improving readability.
Callable<V>
Asynchronous Computation with ReturnCallable<V>
is similar to Runnable
but returns a result and may throw an exception:
V call() throws Exception;
Example: Executing a computation with ExecutorService
import java.util.concurrent.*;
public class CallableExample {
public static void main(String[] args) throws Exception {
Callable<String> task = () -> "Result from callable";
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(task);
System.out.println(future.get()); // Result from callable
executor.shutdown();
}
}
Supplier<T>
Deferred Value GenerationSupplier<T>
is used to generate or provide a value on demand:
T get();
Example: Lazy initialization
import java.util.function.Supplier;
public class SupplierExample {
public static void main(String[] args) {
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // e.g., 0.5834...
}
}
Java’s built-in functional interfaces like Comparator
, Runnable
, Callable
, and Supplier
support functional programming idioms across many APIs. Thanks to lambda expressions, these interfaces can be used more succinctly than ever, enabling cleaner, more expressive, and less error-prone code—whether you’re sorting data, executing tasks, or providing values on demand.
Sorting and grouping complex objects like employees by attributes such as department or salary range is a common task in real-world applications. Using Java Streams and functional programming techniques, we can express this logic declaratively, resulting in cleaner and more maintainable code.
Below is a runnable example that demonstrates:
Comparator
and Collectors.groupingBy
import java.util.*;
import java.util.stream.*;
public class EmployeeProcessing {
static class Employee {
String name;
String department;
double salary;
Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
@Override
public String toString() {
return name + " (" + department + ", $" + salary + ")";
}
}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", "HR", 48000),
new Employee("Bob", "Engineering", 75000),
new Employee("Charlie", "Engineering", 82000),
new Employee("Diana", "HR", 52000),
new Employee("Evan", "Marketing", 60000),
new Employee("Fiona", "Engineering", 69000)
);
// 1. Sort by department, then by salary descending
List<Employee> sorted = employees.stream()
.sorted(Comparator.comparing((Employee e) -> e.department)
.thenComparing(Comparator.comparingDouble((Employee e) -> e.salary).reversed()))
.collect(Collectors.toList());
System.out.println("Sorted Employees:");
sorted.forEach(System.out::println);
// 2. Group by department
Map<String, List<Employee>> groupedByDept = employees.stream()
.collect(Collectors.groupingBy(e -> e.department));
System.out.println("\nGrouped by Department:");
groupedByDept.forEach((dept, list) -> {
System.out.println(dept + ": " + list);
});
// 3. Partition by salary > 70000
Map<Boolean, List<Employee>> highEarners = employees.stream()
.collect(Collectors.partitioningBy(e -> e.salary > 70000));
System.out.println("\nPartitioned by Salary > 70000:");
highEarners.forEach((isHigh, list) -> {
System.out.println((isHigh ? "High Earners" : "Others") + ": " + list);
});
}
}
thenComparing
, sorting by department (ascending) and then salary (descending).Collectors.groupingBy()
organizes employees into lists keyed by department.Collectors.partitioningBy()
separates employees into two groups based on a salary threshold.sorted
, groupingBy
, and partitioningBy
work seamlessly together.This approach demonstrates how Java's functional features simplify complex data manipulation tasks.