Filter, map, reduce and collect — all the power of functional Java
May 22, 2025
7 min read
The Java Streams API (introduced in Java 8) lets you process collections in a declarative, functional style. Instead of writing verbose for-loops, you chain operations like filter, map, and collect to produce clean, readable code.
A Stream is a sequence of elements that supports sequential or parallel operations. It does NOT store data — it processes it lazily on demand.
javaList<String> names = List.of("Alice", "Bob", "Charlie", "Diana"); // Old way — verbose for-loop List<String> result = new ArrayList<>(); for (String name : names) { if (name.startsWith("A")) { result.add(name.toUpperCase()); } } // Streams way — clean and readable List<String> result = names.stream() .filter(name -> name.startsWith("A")) .map(String::toUpperCase) .collect(Collectors.toList()); // Result: ["ALICE"]
javaList<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<Integer> evens = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); // Result: [2, 4, 6, 8, 10] List<String> longNames = names.stream() .filter(name -> name.length() > 4) .collect(Collectors.toList());
map() converts each element to a new value using a function.
javaList<String> names = List.of("alice", "bob", "charlie"); // Capitalize each name List<String> capitalized = names.stream() .map(name -> name.substring(0, 1).toUpperCase() + name.substring(1)) .collect(Collectors.toList()); // Result: ["Alice", "Bob", "Charlie"] // Map to lengths List<Integer> lengths = names.stream() .map(String::length) .collect(Collectors.toList()); // Result: [5, 3, 7]
javaList<Integer> nums = List.of(1, 2, 3, 4, 5); // Sum all numbers int sum = nums.stream() .reduce(0, Integer::sum); // Result: 15 // Multiply all numbers int product = nums.stream() .reduce(1, (a, b) -> a * b); // Result: 120 // Find max Optional<Integer> max = nums.stream() .reduce(Integer::max); max.ifPresent(System.out::println); // 5
Collectors provides many ways to gather stream results:
javaimport java.util.stream.Collectors; List<String> names = List.of("Alice", "Bob", "Alice", "Charlie", "Bob"); // Collect to List List<String> list = names.stream().collect(Collectors.toList()); // Collect to Set (removes duplicates) Set<String> unique = names.stream().collect(Collectors.toSet()); // Join to single String String joined = names.stream() .collect(Collectors.joining(", ")); // "Alice, Bob, Alice, Charlie, Bob" // Group by first letter Map<Character, List<String>> grouped = names.stream() .collect(Collectors.groupingBy(name -> name.charAt(0))); // {A=[Alice, Alice], B=[Bob, Bob], C=[Charlie]} // Count occurrences Map<String, Long> counts = names.stream() .collect(Collectors.groupingBy(n -> n, Collectors.counting()));
javaList<Integer> nums = List.of(5, 3, 1, 4, 2, 3, 5); // Sort List<Integer> sorted = nums.stream() .sorted() .collect(Collectors.toList()); // [1, 2, 3, 3, 4, 5, 5] // Remove duplicates List<Integer> unique = nums.stream() .distinct() .collect(Collectors.toList()); // [5, 3, 1, 4, 2] // First 3 elements List<Integer> first3 = nums.stream() .limit(3) .collect(Collectors.toList()); // [5, 3, 1] // Skip first 2, take next 3 List<Integer> page = nums.stream() .skip(2) .limit(3) .collect(Collectors.toList()); // [1, 4, 2]
javaList<List<Integer>> nested = List.of( List.of(1, 2, 3), List.of(4, 5), List.of(6, 7, 8, 9) ); List<Integer> flat = nested.stream() .flatMap(Collection::stream) .collect(Collectors.toList()); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
Parallel streams use multiple CPU cores automatically. Use for large datasets:
javalong count = IntStream.rangeClosed(1, 1_000_000) .parallel() .filter(n -> n % 2 == 0) .count(); // Processes in parallel automatically // Or convert an existing stream to parallel List<String> processed = names.parallelStream() .map(String::toUpperCase) .collect(Collectors.toList());
Use parallel streams carefully — they add thread-management overhead. They're only faster for large data sets (>10,000 elements) and stateless operations.
javarecord Employee(String name, String dept, double salary) {} List<Employee> employees = List.of( new Employee("Alice", "Engineering", 95000), new Employee("Bob", "Marketing", 72000), new Employee("Charlie", "Engineering", 88000), new Employee("Diana", "HR", 65000), new Employee("Eve", "Engineering", 102000) ); // Average salary of Engineering dept (sorted by salary) OptionalDouble avgEngineering = employees.stream() .filter(e -> e.dept().equals("Engineering")) .mapToDouble(Employee::salary) .average(); // 95000.0 // Top 3 highest-paid employees List<String> top3 = employees.stream() .sorted(Comparator.comparingDouble(Employee::salary).reversed()) .limit(3) .map(Employee::name) .collect(Collectors.toList()); // ["Eve", "Alice", "Charlie"] // Group by department Map<String, List<Employee>> byDept = employees.stream() .collect(Collectors.groupingBy(Employee::dept));
Key Takeaways