Definition
- Two-argument groupingBy takes a classifier and a downstream collector.
- counting() counts elements in each group without storing them, returning Long.
- More efficient than grouping into lists and taking each list's size.
- Histogram output shows the distribution of string lengths.
- TreeMap supplier sorts length keys for clean presentation.
Priya at the National Bank of India sorts reference codes into length bins, but instead of keeping the codes she writes down how many are in each bin: three codes of length 3, one code of length 7, and so on.
Understanding the Concepts
What does this question ask?
The interviewer wants a count of strings at each length. This tests the two-argument groupingBy with counting() as the downstream collector.
How do you create a length histogram?
package com.bank.core;
import java.util.*;
import java.util.stream.Collectors;
public class LengthHistogram {
public static void main(String[] args) {
List<String> codes = List.of(
"SAV", "CUR", "SAV2001", "FD2024", "HDFC", "ICICI", "SBI");
Map<Integer, Long> histogram = codes.stream()
.collect(Collectors.groupingBy(
String::length,
TreeMap::new,
Collectors.counting()));
histogram.forEach((len, count) ->
System.out.println("Length " + len + ": " + count + " codes"));
}
}
Length 3: 3 codes
Length 4: 1 codes
Length 5: 1 codes
Length 6: 1 codes
Length 7: 1 codes
Why is counting() more efficient than grouping into lists?
// Approach 1: group into lists, then get sizes
Map<Integer, List<String>> lists = codes.stream()
.collect(Collectors.groupingBy(String::length));
Map<Integer, Integer> sizes = new HashMap<>();
lists.forEach((k, v) -> sizes.put(k, v.size()));
// Stores every string in memory, then discards
// Approach 2: counting() directly
Map<Integer, Long> counts = codes.stream()
.collect(Collectors.groupingBy(String::length, Collectors.counting()));
// Never stores the strings, only increments counters
counting() is more efficient because it never materializes the group members.
How do you swap the downstream for different aggregations?
| Downstream | Result type | What it computes |
|---|---|---|
| counting() | Long | Number of strings per length |
| summingInt(s -> 1) | Integer | Same as counting but as int |
| joining(", ") | String | Comma-separated strings per length |
| toSet() | Set<String> | Unique strings per length |How do you find the most common length?
Optional<Map.Entry<Integer, Long>> mostCommon = histogram.entrySet()
.stream()
.max(Map.Entry.comparingByValue());
mostCommon.ifPresent(e ->
System.out.println("Most common length: " + e.getKey()
+ " (" + e.getValue() + " strings)"));
Most common length: 3 (3 strings)
What about detecting anomalies?
package com.bank.core;
import java.util.*;
import java.util.stream.Collectors;
public class LengthAnomaly {
public static void main(String[] args) {
List<String> codes = List.of(
"SAV", "CUR", "SBI", "SAV2001", "XX");
Map<Integer, Long> histogram = codes.stream()
.collect(Collectors.groupingBy(
String::length, TreeMap::new, Collectors.counting()));
int expectedLength = 3;
histogram.entrySet().stream()
.filter(e -> e.getKey() != expectedLength)
.forEach(e -> System.out.println(
"ANOMALY: " + e.getValue() + " codes have length "
+ e.getKey() + " (expected " + expectedLength + ")"));
}
}
ANOMALY: 1 codes have length 2 (expected 3)
ANOMALY: 1 codes have length 7 (expected 3)
What are common mistakes?
Mistake: expecting Integer instead of Long from counting().
// WRONG: counting() returns Long, not Integer
// Map<Integer, Integer> wrong = ...groupingBy(..., counting());
// compile error: incompatible types
// CORRECT: use Long
Map<Integer, Long> correct = codes.stream()
.collect(Collectors.groupingBy(String::length, Collectors.counting()));
What about visualizing the histogram?
histogram.forEach((len, count) -> {
String bar = "#".repeat(count.intValue());
System.out.printf("Length %d: %s (%d)%n", len, bar, count);
});
Length 3: ### (3)
Length 4: # (1)
Length 5: # (1)
Length 6: # (1)
Length 7: # (1)
What are common interview follow-up questions?
- "How do you get counts as Integer instead of Long?" Use summingInt(s -> 1) instead of counting(). - "How do you filter lengths with count above a threshold?" Stream the entrySet and filter by value. - "How do you create a character frequency histogram?" Same pattern with chars() and groupingBy(c -> c, counting()).
Visual Overview
List: ["SAV", "CUR", "SAV2001", "FD2024", "HDFC", "ICICI", "SBI"]
│
│ groupingBy(String::length, counting())
▼
┌────────────────────────────┐
│ Length 3 → 3 (SAV,CUR,SBI)│
│ Length 4 → 1 (HDFC) │
│ Length 5 → 1 (ICICI) │
│ Length 6 → 1 (FD2024) │
│ Length 7 → 1 (SAV2001) │
└────────────────────────────┘
Note: strings are counted, not stored
Key Takeaway
To build a length-distribution histogram, use groupingBy with String::length as the classifier and counting() as the downstream. This counts how many strings fall into each length bucket without ever storing the strings themselves, making it more efficient than grouping into lists and measuring each list.
counting() deliberately returns Long so the histogram stays correct even for very large groups. Supply a TreeMap for sorted output. At the National Bank of India, Rajesh used this in a data-quality check to detect malformed batches by spotting an unexpected spike at a particular length.
The key insight is that swapping the downstream is the general lever: counting for counts, summingInt for sums, averagingDouble for averages, all over the same grouping. The classifier stays the same.
Remember one sentence: groupingBy with counting() as the downstream builds a histogram that counts per group without storing the members.
---