Streams & Lambda
Master lambda expressions, functional interfaces, the Stream API, and collectors for modern functional-style Java programming.
Lambda Expressions
Lambda expressions, introduced in Java 8, provide a concise way to represent anonymous functions that implement a single abstract method. They use the arrow syntax with parameters on the left and the body on the right, eliminating the verbosity of anonymous inner classes. Lambda expressions can capture variables from the enclosing scope, provided those variables are effectively final. They are the foundation of functional-style programming in Java and are used extensively with the Stream API and collections.
import java.util.*;
public class LambdaDemo {
public static void main(String[] args) {
// Before lambda: anonymous inner class
Comparator<String> oldWay = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
// With lambda expression
Comparator<String> byLength = (a, b) -> a.length() - b.length();
List<String> names = new ArrayList<>(
List.of("Charlie", "Alice", "Bob", "Dave")
);
names.sort(byLength);
System.out.println(names); // [Bob, Dave, Alice, Charlie]
// Method reference (shorthand for lambda)
names.sort(Comparator.comparingInt(String::length));
// Lambda with forEach
names.forEach(name -> System.out.println("Hello, " + name));
// Lambda stored in a variable
Runnable task = () -> System.out.println("Task running!");
task.run();
}
}
Functional Interfaces
A functional interface is an interface with exactly one abstract method, making it eligible as the target type for lambda expressions. Java provides many built-in functional interfaces in the java.util.function package including Predicate, Function, Consumer, and Supplier. The @FunctionalInterface annotation is optional but recommended as it causes a compile error if the interface has more than one abstract method. Understanding these core interfaces allows you to compose powerful data transformations with minimal code.
import java.util.function.*;
import java.util.List;
public class FunctionalDemo {
public static void main(String[] args) {
// Predicate: takes T, returns boolean
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Hello")); // false
System.out.println(isLong.test("Hello World")); // true
// Function: takes T, returns R
Function<String, Integer> toLength = String::length;
System.out.println(toLength.apply("Java")); // 4
// Composing functions
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> pipeline = trim.andThen(upper);
System.out.println(pipeline.apply(" hello ")); // "HELLO"
// Consumer: takes T, returns void
Consumer<String> printer = System.out::println;
List.of("A", "B", "C").forEach(printer);
// Supplier: takes nothing, returns T
Supplier<Double> random = Math::random;
System.out.println(random.get());
// BiFunction: takes T and U, returns R
BiFunction<Integer, Integer, Integer> max = Math::max;
System.out.println(max.apply(10, 20)); // 20
}
}
The Stream API
The Stream API provides a declarative approach to processing collections of data through a pipeline of operations. Streams are lazy, meaning intermediate operations like filter, map, and sorted are not executed until a terminal operation triggers the computation. This lazy evaluation enables optimizations such as short-circuiting and loop fusion. Streams do not modify the underlying collection and can only be consumed once, promoting a functional programming style that avoids side effects.
import java.util.*;
import java.util.stream.*;
public class StreamDemo {
public static void main(String[] args) {
List<String> names = List.of(
"Alice", "Bob", "Charlie", "David", "Eve", "Frank"
);
// Filter, map, and collect
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
System.out.println(result); // [ALICE, CHARLIE, DAVID, FRANK]
// Chaining operations
long count = names.stream()
.filter(n -> n.startsWith("A") || n.startsWith("C"))
.count();
System.out.println("Count: " + count); // 2
// Finding elements
Optional<String> first = names.stream()
.filter(n -> n.length() == 3)
.findFirst();
first.ifPresent(n -> System.out.println("Found: " + n)); // "Found: Bob"
// Numeric streams
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sum = IntStream.of(numbers)
.filter(n -> n % 2 == 0)
.sum();
System.out.println("Sum of evens: " + sum); // 30
// Generate and iterate
List<Integer> squares = IntStream.rangeClosed(1, 5)
.map(n -> n * n)
.boxed()
.collect(Collectors.toList());
System.out.println(squares); // [1, 4, 9, 16, 25]
}
}
Collectors and Reduction
Collectors are terminal operations that accumulate stream elements into a result container such as a list, set, map, or string. The Collectors utility class provides factory methods for the most common collection strategies including grouping, partitioning, and summarizing. The reduce operation combines all elements into a single result using an associative accumulator function. These tools enable powerful data aggregation and transformation with concise, readable code.
import java.util.*;
import java.util.stream.*;
public class CollectorDemo {
record Person(String name, String department, double salary) {}
public static void main(String[] args) {
List<Person> employees = List.of(
new Person("Alice", "Engineering", 95000),
new Person("Bob", "Marketing", 72000),
new Person("Charlie", "Engineering", 105000),
new Person("Diana", "Marketing", 68000),
new Person("Eve", "Engineering", 88000)
);
// Joining strings
String names = employees.stream()
.map(Person::name)
.collect(Collectors.joining(", "));
System.out.println(names); // Alice, Bob, Charlie, Diana, Eve
// Grouping by department
Map<String, List<Person>> byDept = employees.stream()
.collect(Collectors.groupingBy(Person::department));
byDept.forEach((dept, people) ->
System.out.println(dept + ": " + people.size()));
// Average salary by department
Map<String, Double> avgSalary = employees.stream()
.collect(Collectors.groupingBy(
Person::department,
Collectors.averagingDouble(Person::salary)
));
System.out.println(avgSalary);
// {Engineering=96000.0, Marketing=70000.0}
// Partitioning (boolean grouping)
Map<Boolean, List<Person>> highEarners = employees.stream()
.collect(Collectors.partitioningBy(p -> p.salary() > 80000));
// Reduce to find total salary
double totalSalary = employees.stream()
.map(Person::salary)
.reduce(0.0, Double::sum);
System.out.println("Total payroll: quot; + totalSalary);
}
}