Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
74: Grouping and Partitioning with Streams
I once worked with a developer who spent an entire afternoon writing a complex set of nested for loops and if statements just to categorize a list of customer orders by their shipping region. He was manually initializing HashMaps, checking if keys existed, creating new ArrayLists on the fly, and adding items one by one. It was about forty lines of boilerplate that was incredibly fragile. When I showed him how to do the exact same thing in a single line using Collectors.groupingBy, he actually stared at the screen in silence for a few seconds. It's one of those "lightbulb" moments in Java where the language stops feeling like a chore and starts feeling like a tool.
Organizing Data into Buckets with groupingBy
At its core, groupingBy is used when you want to take a flat stream of objects and turn it into a Map where the keys are some property of the object, and the values are lists of the objects that share that property. Think of it as sorting a deck of cards into piles by suit.
Let's say we're dealing with a Transaction class that has a currency field. If you have a list of a thousand transactions and you want to see all transactions grouped by their currency, you don't need to manage the map yourself. You just tell the stream how to derive the key.
Map<String, List<Transaction>> transactionsByCurrency = transactions.stream()
.collect(Collectors.groupingBy(Transaction::getCurrency));
That's it. Java handles the map creation and the list appending behind the scenes. I personally find this much cleaner because it describes what you want to happen rather than how to manipulate the map pointers.
The Boolean Split with partitioningBy
Now, you might be tempted to use groupingBy for everything, but there's a specialized version called partitioningBy. This is specifically for when your "grouping" is actually a binary choice—a true or false. While groupingBy returns a Map<K, List<T>>, partitioningBy always returns a Map<Boolean, List<T>>.
Imagine you need to separate your transactions into two piles: those that are flagged for fraud review and those that aren't. Using a predicate makes this incredibly readable:
Map<Boolean, List<Transaction>> fraudCheck = transactions.stream()
.collect(Collectors.partitioningBy(t -> t.getAmount() > 10000));
In this map, the true key will hold all high-value transactions, and the false key will hold everything else. It's more efficient than groupingBy because the JVM knows there are only ever two possible keys.
Refining Results with Downstream Collectors
The real power comes when you realize that groupingBy can take a second argument: a "downstream" collector. By default, it just collects items into a List, but what if you don't actually need the list? What if you just want to know how many transactions occurred in each currency?
Instead of grouping them into a list and then calling .size() on every entry in the map, you can pass Collectors.counting() as the second argument. This transforms the result from a Map<String, List<Transaction>> to a Map<String, Long>.
Map<String, Long> countsByCurrency = transactions.stream()
.collect(Collectors.groupingBy(
Transaction::getCurrency,
Collectors.counting()
));
You can do this with almost any collector. Want the most expensive transaction per currency? Use Collectors.maxBy. Want the average transaction amount per currency? Use Collectors.averagingDouble. It allows you to perform complex data analysis in a functional style without ever writing a manual loop.
📋 Practical Task
Analyzing E-Commerce Order Distributions
You are tasked with creating a report for a logistics manager. You have a list of Order objects, where each Order has a String city, a double totalValue, and a boolean isExpressShipping.
Write a method that processes a List<Order> and returns the following two pieces of data:
- A
Map<String, Long>containing the total number of orders placed in each city. - A
Map<Boolean, List<Order>>partitioning the orders into those that used express shipping and those that did not.
Constraints: You must use Java Streams and the Collectors utility class. Avoid using any manual for or while loops to group the data.
There are no comments for now.