Definition
Counting and ranking are two distinct steps.
groupingBy(classifier, counting())– builds aMapmapping each key to its occurrence count.counting()– a downstream collector that returns the number of elements in each group as aLong.- ranking by value – stream the map's
entrySet, sort withMap.Entry.comparingByValue(Comparator.reverseOrder()), and optionallylimitto the top N. - tie-breaking – chain
thenComparing(Map.Entry.comparingByKey())so entries with equal counts order deterministically.
Rajesh tallies how often each word appears across all transaction notes, like counting how many times SALARY or RENT shows up. counting is the tally mark per word. Then he lines the words up with the most frequent on top, and when two words tie he orders them alphabetically so the list looks the same every time.
Understanding the Concepts
What is Collectors.counting as a downstream collector?
counting() is a collector that counts the elements it receives and returns the total as a Long. Used as the downstream of groupingBy, it counts the elements in each group, producing a map from key to count. This is the canonical frequency-map idiom.
package com.bank.text;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
public class CountingDemo {
public static void main(String[] args) {
List<String> words = List.of("SALARY", "RENT", "SALARY", "EMI", "SALARY", "RENT");
Map<String, Long> freq = words.stream()
.collect(groupingBy(w -> w, counting()));
System.out.println(freq);
}
}
{EMI=1, RENT=2, SALARY=3}
How do you build the frequency map from narration strings?
Transaction narrations are sentences. Split each narration into words, flatten them into one stream, normalize case, and group by word with counting.
package com.bank.text;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
public class NarrationFrequency {
public Map<String, Long> wordCounts(List<String> narrations) {
return narrations.stream()
.flatMap(n -> Arrays.stream(n.toUpperCase().split("\\s+")))
.filter(w -> !w.isBlank())
.collect(groupingBy(w -> w, counting()));
}
}
// Example input
List<String> narrations = List.of(
"SALARY CREDIT NATIONAL BANK",
"RENT PAYMENT",
"SALARY CREDIT",
"EMI PAYMENT NATIONAL BANK");
{SALARY=2, CREDIT=2, NATIONAL=2, BANK=2, RENT=1, PAYMENT=2, EMI=1}
Why can you not sort a map directly by value?
A Map is not ordered by value, and most map implementations are not ordered at all. TreeMap orders by key, not value. To rank by frequency you must take the map's entries into a stream and sort those entries.
// WRONG expectation: a TreeMap does NOT sort by value
Map<String, Long> freq = new java.util.TreeMap<>(wordCounts);
// This orders by WORD alphabetically, not by COUNT
// CORRECT: stream the entries and sort by value
List<Map.Entry<String, Long>> ranked = freq.entrySet().stream()
.sorted(Map.Entry.comparingByValue(java.util.Comparator.reverseOrder()))
.toList();
How do you sort the entries by count descending and take the top N?
Stream entrySet(), sort with Map.Entry.comparingByValue wrapped in Comparator.reverseOrder() so the largest count comes first, then limit to the top N.
package com.bank.text;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
public class TopWords {
public List<Map.Entry<String, Long>> topN(Map<String, Long> freq, int topN) {
return freq.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue(Comparator.reverseOrder()))
.limit(topN)
.toList();
}
}
// top 3 of the sample narrations
SALARY=2
CREDIT=2
NATIONAL=2
How do you make ties deterministic?
When several words share the same count, their relative order is otherwise unspecified. Chain a secondary comparator on the key so ties break alphabetically, which makes the ranking stable across runs.
// WRONG: ties among equal counts order unpredictably
freq.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(5);
// CORRECT: count descending, then word ascending as a tie-breaker
freq.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue(Comparator.reverseOrder())
.thenComparing(Map.Entry.comparingByKey()))
.limit(5);
BANK=2
CREDIT=2
NATIONAL=2
PAYMENT=2
SALARY=2
How do you keep the ranking as a map?
If a downstream needs the ranked result as a map rather than a list, collect the sorted entries into a LinkedHashMap, which preserves insertion order and therefore the ranking.
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Comparator;
import static java.util.stream.Collectors.toMap;
LinkedHashMap<String, Long> rankedMap = freq.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue(Comparator.reverseOrder())
.thenComparing(Map.Entry.comparingByKey()))
.collect(toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(a, b) -> a, // merge function (keys are unique here)
LinkedHashMap::new)); // preserve ranking order
How do comparingByValue and comparingByKey compare?
| Helper | Sorts by | Natural order | Reverse order |
| --- | --- | --- | --- |
| Map.Entry.comparingByValue() | entry value | ascending count | comparingByValue(reverseOrder()) |
| Map.Entry.comparingByKey() | entry key | ascending key | comparingByKey(reverseOrder()) |
// By value ascending
Map.Entry.comparingByValue();
// By value descending
Map.Entry.comparingByValue(Comparator.reverseOrder());
// By key ascending (good as a tie-breaker)
Map.Entry.comparingByKey();
What are common mistakes?
A frequent mistake is using Collectors.summingInt(e -> 1) where counting() is clearer, or mixing up counting() (a Long) with an int, which causes an unboxing surprise.
// WRONG: counting returns Long, assigning to long is fine but to int needs a cast
Map<String, Long> freq = words.stream().collect(groupingBy(w -> w, counting()));
// int c = freq.get("SALARY"); // compile error: Long cannot convert to int
// CORRECT: keep it as Long, or convert explicitly
long c = freq.get("SALARY");
int ci = freq.get("SALARY").intValue();
Another mistake is forgetting the explicit type witness on comparingByValue, which can trip type inference when chaining thenComparing.
// Clearer with an explicit type witness when chaining
.sorted(Map.Entry.<String, Long>comparingByValue(Comparator.reverseOrder())
.thenComparing(Map.Entry.comparingByKey()))
When should you use this pattern and what are the follow-ups?
Use the frequency-map-then-rank pattern whenever you need a histogram and a top-N list, such as finding the most common keywords in transaction narrations to feed an auto-categorization feature. Interviewers often follow up by asking why a map cannot be sorted by value in place (it has no value order), how to break ties deterministically (thenComparing on the key), and how to keep the ranking as a map (LinkedHashMap). A further follow-up is the difference between counting() and summingLong, where counting() always adds one per element and summingLong adds a computed amount.
Visual Overview
narrations -> words -> frequency map
["SALARY CREDIT", "RENT", "SALARY"]
│ flatMap split on whitespace
▼
[SALARY, CREDIT, RENT, SALARY]
│ groupingBy(w -> w, counting())
▼
{ SALARY=2, CREDIT=1, RENT=1 } (unordered map)
rank by value descending
entrySet().stream()
│ sorted(comparingByValue(reverseOrder())
│ .thenComparing(comparingByKey()))
▼
┌───────────┬───────┐
│ SALARY │ 2 │ <- highest count first
├───────────┼───────┤
│ CREDIT │ 1 │ <- tie broken by key (CREDIT before RENT)
├───────────┼───────┤
│ RENT │ 1 │
└───────────┴───────┘
│ limit(topN)
▼
top N ranked entries
keep ranking as a map
sorted entries ──► collect(toMap(k, v, merge, LinkedHashMap::new))
│
▼
LinkedHashMap preserves the ranked order
Key Takeaway
To build a frequency map I group by the value I want to count and use counting as the downstream collector, which gives me a map from each key to how many elements fell into that group as a Long. Splitting the narrations into words, normalizing case, and grouping by word with counting gives me the occurrence count for every word in one pass.
Ranking by frequency is a separate step, because a map has no inherent order by value. I stream the map's entry set and sort with Map.Entry.comparingByValue in reverse order so the most frequent entries come first, then limit to the top N. In my banking work at the National Bank of India I used exactly this to find the most common keywords in transaction narrations, which fed an auto-categorization feature.
The senior point I always add is tie-breaking and determinism. When two keys share the same count, I chain a secondary comparator on the key with thenComparing so the order is stable across runs, which matters when the output drives a report or a model. If I need the ranked result as a map rather than a list, I collect the sorted entries into a LinkedHashMap to preserve that order, since a plain HashMap would scramble it and a TreeMap would re-order by key rather than count.
Think of Rajesh tallying words across every transaction note: he counts each word, stacks the most frequent on top, and settles ties alphabetically so the list reads the same every time.
Remember one sentence: groupingBy(word, counting()) builds the histogram, and sorting the entry set by value descending with a key tie-breaker ranks it deterministically.
---