Java Stream API Deep Dive: Practical Examples for efficient coding

Deepak KumarDeepak Kumar
4 min read

Introduction to Java Stream API

The Stream API, introduced in Java 8, revolutionized how we process collections by providing a declarative, functional, and parallel-processing approach. Instead of writing verbose loops, developers can chain operations like filtering, mapping, and reducing with clean, readable, and expressive syntax.

Key Advantages:

Concise & Readable – Eliminates boilerplate loops
Lazy Evaluation – Optimizes performance by processing only when needed
Parallel Execution – Enables multi-threaded processing with parallelStream()
Functional Style – Encourages immutability and reduces side effects

In this article, we'll explore practical use cases of the Stream API with real-world examples.


1. Frequency Map: Count Occurrences in a List

Use Case: Analysing word frequencies, log analysis.

List<String> items = List.of("apple", "banana", "apple");
Map<String, Long> itemCounts = items.stream()
    .collect(Collectors.groupingBy(
        Function.identity(), 
        Collectors.counting()
    ));
// Output: {banana=1, apple=2}

2. Finding Duplicates in an Array

Use Case: Data validation, deduplication.

int[] array = {1, 2, 3, 2, 4, 5, 3, 6, 7};
List<Integer> duplicates = Arrays.stream(array)
    .boxed()
    .collect(Collectors.groupingBy(
        Function.identity(), 
        Collectors.counting()
    ))
    .entrySet().stream()
    .filter(entry -> entry.getValue() > 1)
    .map(Map.Entry::getKey)
    .collect(Collectors.toList());
// Output: [2, 3]

3. Grouping & Aggregating Data

3.1 Filtering Within Groups

Use Case: Categorizing high-calorie dishes by type.

Map<Dish.Type, List<Dish>> highCalorieDishesByType = menu.stream()
    .collect(Collectors.groupingBy(
        Dish::getType,
        Collectors.filtering(
            dish -> dish.getCalories() > 500, 
            Collectors.toList()
        )
    ));

3.2 Nested Grouping with Mapping

Use Case: Extracting dish names grouped by type.

Map<Dish.Type, List<String>> groupedDishNames = menu.stream()
    .collect(Collectors.groupingBy(
        Dish::getType,
        Collectors.mapping(Dish::getName, Collectors.toList())
    ));

3.3 Top 3 Highest items per category

Use Case: Leaderboards, recommendations.

Map<Dish.Type, List<Dish>> topDishesByType = menu.stream()
    .collect(Collectors.groupingBy(
        Dish::getType,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                .sorted(Comparator.comparingInt(Dish::getCalories).reversed())
                .limit(3)
                .collect(Collectors.toList())
        )
    ));

4. Partitioning Numbers into Even & Odd

Use Case: Data segmentation for separate processing.

List<Integer> numbers = IntStream.rangeClosed(1, 10).boxed().toList();
Map<Boolean, List<Integer>> partitioned = numbers.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0));
// Output: {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}

5. Selective Filtering: takeWhile & dropWhile

Use Case: Processing sorted data (e.g., logs, time-series).

Advantages: Short-circuiting. It takes elements from the start to the first mismatch.

List<Dish> lowCalorie = menu.stream()
    .takeWhile(dish -> dish.getCalories() < 320)
    .collect(Collectors.toList());

List<Dish> highCalorie = menu.stream()
    .dropWhile(dish -> dish.getCalories() < 320)
    .collect(Collectors.toList());

6. Detecting Duplicates in a Collection

Use Case: Preventing duplicate entries in APIs/databases.

List<Integer> list = List.of(1, 2, 3, 4, 5, 3, 2, 6);
Set<Integer> seen = new HashSet<>();
Set<Integer> duplicates = list.stream()
    .filter(n -> !seen.add(n))
    .collect(Collectors.toSet());
// Output: [2, 3]

7. Custom Collector with Collector.of

Use Case: Custom string joining (e.g., CSV, logs).

Collector<String, StringJoiner, String> customJoiner = Collector.of(
    () -> new StringJoiner(" | "),
    StringJoiner::add,
    StringJoiner::merge,
    StringJoiner::toString
);
String joined = Stream.of("apple", "banana", "cherry").collect(customJoiner);
// Output: "apple | banana | cherry"

8. Top-N by Group (e.g., Top 3 Employees per Department)

Use Case: Dashboards, leaderboards.

Map<String, List<Employee>> topEmployees = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                .sorted(Comparator.comparingDouble(Employee::getPerformanceScore).reversed())
                .limit(3)
                .collect(Collectors.toList())
        )
    ));

9. Find First Non-Repeating Character in a String

Use Case: Data validation, text processing.

String input = "swiss";
Character firstUnique = input.chars()
    .mapToObj(c -> (char) c)
    .collect(Collectors.groupingBy(
        c -> c, 
        LinkedHashMap::new, 
        Collectors.counting()
    ))
    .entrySet().stream()
    .filter(e -> e.getValue() == 1)
    .map(Map.Entry::getKey)
    .findFirst()
    .orElse(null);
// Output: 'w'

10. Generate Cartesian Product

Use Case: Combining all pairs (e.g., users × roles).

List<String> users = List.of("Alice", "Bob");
List<String> roles = List.of("Admin", "User");
List<String> pairs = users.stream()
    .flatMap(user -> roles.stream().map(role -> user + "-" + role))
    .collect(Collectors.toList());
// Output: [Alice-Admin, Alice-User, Bob-Admin, Bob-User]

11. Advanced Use Cases

11.1 Compute Median from a Dynamic Dataset

List<Integer> nums = List.of(3, 5, 1, 4, 2);
double median = nums.stream()
    .sorted()
    .skip((nums.size() - 1) / 2)
    .limit(nums.size() % 2 == 0 ? 2 : 1)
    .mapToInt(Integer::intValue)
    .average()
    .orElse(0);
// Output: 3.0

11.2 Paginate Large Lists

int page = 2, pageSize = 5;
List<String> paged = items.stream()
    .skip((page - 1) * pageSize)
    .limit(pageSize)
    .collect(Collectors.toList());

12. Debugging Stream Pipelines with peek()

Use Case: Logging intermediate steps.

List<String> results = Stream.of("stream", "api", "power", "rocks")
    .peek(s -> System.out.println("Original: " + s))
    .map(String::toUpperCase)
    .peek(s -> System.out.println("Uppercased: " + s))
    .filter(s -> s.length() > 4)
    .collect(Collectors.toList());

13. Zipping Two Lists

Use Case: Merging parallel data (e.g., names + scores).

List<String> names = List.of("Alice", "Bob");
List<Integer> scores = List.of(85, 92);
List<String> zipped = IntStream.range(0, Math.min(names.size(), scores.size()))
    .mapToObj(i -> names.get(i) + ":" + scores.get(i))
    .collect(Collectors.toList());
// Output: [Alice:85, Bob:92]

Best Practices

Prefer Method References
Use Parallel Streams Wisely (Only for large datasets)
Replace peek() with Logging in Production


Conclusion

The Stream API is a powerful tool for processing collections efficiently and elegantly. Mastering these techniques allows writing cleaner, more maintainable, and often faster Java code.

0
Subscribe to my newsletter

Read articles from Deepak Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Deepak Kumar
Deepak Kumar