java

Java Stream API Deep Dive

Master the Stream API for functional programming in Java

Reading Time: 7 min readAuthor: DeepTechHub
#java#streams#functional-programming
Java Stream API Deep Dive

Java Stream API Deep Dive: Practical Examples for efficient coding

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 ReferencesUse 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.

Enjoyed this article?

Check out more articles or share this with your network.