-
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
266: Practice Exercise: Building a Word Frequency Counter with Streams
We've spent a lot of time talking about the theory of Streams, but the best way to actually "get" the functional style in Java is to build something that feels like a real-world utility. Today, we're building a word frequency counter. It's a classic problem, but it's perfect for demonstrating how we can chain transformations to turn a messy string of text into a structured map of counts.
Turning a string into a stream of words
I usually start by thinking about the data pipeline. We have a big block of text, and we want a Map<String, Long>. The first hurdle is getting that text into a Stream. I'll use split("\\s+") to break the string apart by any whitespace, and then wrap that array in Arrays.stream().
String text = "Java is great, and Java streams are powerful. Streams make Java better.";
long count = Arrays.stream(text.split("\\s+"))
.filter(word -> !word.isEmpty())
.count();
That's a start, but it's too simple. If we just count them now, we're going to run into a problem that I actually stumbled into the first time I wrote this logic in a production environment.
Dealing with the "Punctuation Trap"
Here is where I messed up during my first attempt: I forgot that split("\\s+") only handles spaces. If the text says "Java is great, and Java streams...", my stream will see "great," (with a comma) and "great" (without one) as two entirely different words. That's a bug.
To fix this, I need to normalize the data. I'll add a map step to convert everything to lowercase and use a regular expression to strip out any non-alphanumeric characters. It's much cleaner to do this early in the pipeline so the grouping logic doesn't have to worry about it.
Stream<String> wordStream = Arrays.stream(text.split("\\s+"))
.map(String::toLowerCase)
.map(word -> word.replaceAll("[^a-zA-Z0-9]", ""));
Now, "Great," becomes "great", and we're finally comparing apples to apples.
The magic of groupingBy and counting
Now for the heavy lifting. We need to group identical words together and count how many times each appears. In the old days, we would have created a HashMap, looped through the list, and checked if (map.containsKey(word)). That's too much boilerplate. Instead, we use Collectors.groupingBy.
The groupingBy collector takes two arguments here: the classifier (what we are grouping by) and a downstream collector (what to do with the values in that group). Since we want the total count, Collectors.counting() is our best friend.
Map<String, Long> frequencies = wordStream
.filter(word -> !word.isEmpty()) // Remove strings that became empty after regex
.collect(Collectors.groupingBy(
word -> word,
Collectors.counting()
));
System.out.println(frequencies);
I prefer this approach because it's declarative. I'm not telling Java how to manage the map; I'm telling it what the result should be: "Group by the word itself, and count the occurrences." It's concise, and once you're used to it, it's much harder to introduce off-by-one errors or null pointer exceptions than it is with a manual loop.
📋 Practical Task
Exercise: Building a Log Level Analyzer
Imagine you have a raw log file represented as a single String. Each line starts with a log level (INFO, WARN, or ERROR) followed by a message. Your task is to create a program that counts the occurrences of each log level using Java Streams.
Requirements:
- Start with this input string:
"INFO: System started\nWARN: Low disk space\nERROR: Database connection failed\nINFO: User logged in\nERROR: NullPointerException in AuthService\nINFO: Heartbeat sent" - Split the string into lines.
- Extract only the log level (the part before the colon).
- Use
Collectors.groupingByandCollectors.counting()to produce aMap<String, Long>. - Print the final map to the console.
There are no comments for now.