Back to Stream Practice: String & Character
Q5Question 5 of 11

Frequency count of each string in a list using streams

Key Concepts
  • groupingBy(identity(), counting()) produces Map<String, Long> with frequency of each string
  • TreeMap::new as the map factory produces sorted keys automatically
  • sorted(comparingByValue().reversed()) on the entrySet ranks strings by frequency descending
  • For case-insensitive counting, add map(String::toLowerCase) before groupingBy
  • This replaces the manual pattern of map.merge(key, 1L, Long::sum) in a for-loop
  • The counting() collector is more efficient than toList() followed by size() because it only tracks a counter

Definition

Use groupingBy(Function.identity(), Collectors.counting()) to produce a Map<String, Long> with the frequency of each string in the list.
  • groupingBy(identity(), counting()) groups strings by their value and counts occurrences, producing Map.
  • TreeMap::new as the map factory produces alphabetically sorted keys without a separate sort step.
  • sorted(comparingByValue().reversed()) on the entry set produces a frequency-ranked list from most common to least.
  • map(String::toLowerCase) before groupingBy enables case-insensitive counting.
  • This single collector call replaces the verbose manual pattern of iterating with map.merge(key, 1L, Long::sum).

At National Bank of India, the operations team logs every transaction type in a day: ["NEFT", "UPI", "NEFT", "RTGS", "UPI", "NEFT"]. At end of day, they need a summary: NEFT appeared 3 times, UPI appeared 2 times, RTGS appeared 1 time. The groupingBy plus counting collector produces this summary in one pass.

Understanding the Concepts

How do you count string frequencies?

JAVA
package com.bank.analytics;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class StringFrequencyDemo {
    public static void main(String[] args) {
        List<String> txnTypes = List.of(
            "NEFT", "UPI", "NEFT", "RTGS", "UPI", "NEFT");

        // Basic frequency map
        Map<String, Long> freq = txnTypes.stream()
            .collect(Collectors.groupingBy(
                Function.identity(), Collectors.counting()));

        System.out.println("Frequency: " + freq);
    }
}
TEXT
Frequency: {UPI=2, RTGS=1, NEFT=3}

Note: HashMap produces random key order.

How do you get sorted keys?

Pass TreeMap::new as the map factory.

JAVA
// Sorted keys (alphabetical)
Map<String, Long> sortedFreq = txnTypes.stream()
    .collect(Collectors.groupingBy(
        Function.identity(),
        TreeMap::new,
        Collectors.counting()));

System.out.println("Sorted: " + sortedFreq);
TEXT
Sorted: {NEFT=3, RTGS=1, UPI=2}

How do you rank by frequency?

JAVA
// Ranked by frequency descending
List<Map.Entry<String, Long>> ranked = freq.entrySet().stream()
    .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
    .toList();

ranked.forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
TEXT
NEFT: 3
UPI: 2
RTGS: 1

How do you handle case-insensitive counting?

JAVA
// Case-insensitive: normalize to lowercase before counting
List<String> mixed = List.of("Neft", "neft", "NEFT", "UPI", "upi");

Map<String, Long> caseInsensitive = mixed.stream()
    .map(String::toLowerCase)
    .collect(Collectors.groupingBy(
        Function.identity(), Collectors.counting()));

System.out.println(caseInsensitive);  // {neft=3, upi=2}

How is this different from the manual approach?

JAVA
// Manual approach (verbose, error-prone)
Map<String, Long> manual = new HashMap<>();
for (String type : txnTypes) {
    manual.merge(type, 1L, Long::sum);
}

The stream approach is one line, declarative, and works with parallel streams. The manual approach is imperative and requires an explicit mutable Map.

What about counting with a filter?

JAVA
// Count only transaction types that appear more than once
Map<String, Long> repeats = txnTypes.stream()
    .collect(Collectors.groupingBy(
        Function.identity(), Collectors.counting()))
    .entrySet().stream()
    .filter(e -> e.getValue() > 1L)
    .collect(Collectors.toMap(
        Map.Entry::getKey, Map.Entry::getValue));

System.out.println("Repeated: " + repeats);  // {UPI=2, NEFT=3}

What are common mistakes?

WRONG: Using Collectors.toSet() and thinking it counts. It just deduplicates.

WRONG: Forgetting the type parameter on comparingByValue when chaining .reversed().

JAVA
// WRONG: type inference fails without explicit type
freq.entrySet().stream()
    .sorted(Map.Entry.comparingByValue().reversed());  // may not compile

// CORRECT: provide type parameters
freq.entrySet().stream()
    .sorted(Map.Entry.<String, Long>comparingByValue().reversed());

What should a senior engineer know?

  • groupingBy(identity(), counting()) is the idiomatic one-liner for frequency counting in Java streams. Interviewers expect this exact pattern.
  • The counting() downstream is more efficient than toList() followed by List::size because it only tracks a long counter, not a full ArrayList.
  • For Integer frequency counting (like character codes), Collectors.groupingBy returns Map. Use mapToObj(c -> (char) c) first if you want Map.
  • In parallel streams, groupingBy uses a thread-safe merge strategy automatically. For better parallel performance on large datasets, use groupingByConcurrent.

Visual Overview

DIAGRAM
  ["NEFT", "UPI", "NEFT", "RTGS", "UPI", "NEFT"]
       │
       ▼
  groupingBy(identity(), counting())
       │
       ▼
  Map<String, Long>
  ┌───────────────┐
  │ NEFT  → 3     │
  │ UPI   → 2     │
  │ RTGS  → 1     │
  └───────┬───────┘
          │
          ▼
  sorted(comparingByValue().reversed())
          │
          ▼
  [NEFT:3, UPI:2, RTGS:1]  (frequency ranked)

Key Takeaway

String frequency counting is one of the most common stream interview questions. The answer is always groupingBy(Function.identity(), Collectors.counting()) which produces Map. For sorted keys, pass TreeMap::new as the map factory. For frequency-ranked output, stream the entrySet and sort by value descending.

This single-line collector replaces the manual pattern of iterating with map.merge(key, 1L, Long::sum). For case-insensitive counting, add map(String::toLowerCase) before groupingBy.

At National Bank of India, the operations dashboard shows transaction type volumes: NEFT (3 times), UPI (2 times), RTGS (1 time). The groupingBy collector produces this summary in one pass through the data.

Remember one sentence: groupingBy with identity and counting is the one-liner for frequency maps, and TreeMap gives sorted keys.

---

Share
Use ← → arrow keys to navigate