Back to Collectors
Q21Question 21 of 21

Build a formatted department summary string from a grouped map using entrySet streaming and Collectors.joining

Key Concepts
  • A Map has no stream() method, so you stream entrySet(), keySet(), or values()
  • Sort the entry stream with Map.Entry.comparingByKey() to make output deterministic
  • Map each entry to a String such as key + "=" + value before joining
  • joining(delimiter, prefix, suffix) concatenates pieces and wraps them with brackets
  • joining operates on a Stream<CharSequence>, so entries must be mapped to strings first
  • joining uses a StringBuilder internally, so it is efficient even for many pieces
  • On an empty stream joining emits just the prefix and suffix, giving []

Definition

Stream `map.entrySet()`, format each entry to text, and `joining(", ", "[", "]")` stitches them into one bracketed summary.

Turning a map into one readable line is a stream-the-views plus joining task.

  • No Map.stream() – a Map is not a Collection, so you obtain a stream from a view: entrySet(), keySet(), or values().
  • Sort for determinism – sorted(Map.Entry.comparingByKey()) orders entries so the output does not depend on the map's internal layout.
  • Map to text – map(e -> e.getKey() + "=" + e.getValue()) turns each entry into a String.
  • joining(delimiter, prefix, suffix) – concatenates the strings with the delimiter and wraps the whole with prefix and suffix.
  • CharSequence only – joining works on a stream of character sequences, so entries must be strings before collecting.

Priya has a tally sheet at the National Bank of India showing how many employees are in each department. She wants a single tidy line, like a label: departments in order, each with its count, separated by commas, wrapped in brackets. joining is the glue that stitches the pieces into one neat string that stands out in the end-of-batch log.

Understanding the Concepts

Why does a Map have no stream method?

A Map is not a Collection, so it does not have a stream() method. Instead you obtain a stream from one of its views: entrySet().stream() for key-value pairs, keySet().stream() for keys, or values().stream() for values. For a formatted summary you almost always use the entry set, because you need both the department and its count.

JAVA
package com.bank.report;

import java.util.Map;

public class StreamAMap {
    public static void main(String[] args) {
        Map<String, Long> counts = Map.of("Trading", 3L, "Risk", 2L);

        counts.entrySet().stream()               // Stream<Map.Entry<String,Long>>
            .forEach(e -> System.out.println(e.getKey() + "=" + e.getValue()));
    }
}
TEXT
Trading=3
Risk=2

How do you build the formatted summary string?

Sort the entries by key, map each entry to text, then collect with joining(delimiter, prefix, suffix). The result is one bracketed line.

JAVA
package com.bank.report;

import java.util.Map;
import java.util.stream.Collectors;

public class DepartmentSummary {
    public static void main(String[] args) {
        Map<String, Long> counts = Map.of("Trading", 3L, "Risk", 2L);

        String summary = counts.entrySet().stream()
            .sorted(Map.Entry.comparingByKey())
            .map(e -> e.getKey() + "=" + e.getValue())
            .collect(Collectors.joining(", ", "[", "]"));

        System.out.println(summary);
    }
}
TEXT
[Risk=2, Trading=3]

Sorting by key puts Risk before Trading regardless of the map's internal ordering.

How do the three forms of joining differ?

joining has three overloads. The no-argument form just concatenates, the one-argument form adds a delimiter, and the three-argument form adds a delimiter plus wrapping prefix and suffix.

| Call | Behaviour | Example output |
|------|-----------|----------------|
| joining() | concatenate, no separator | Risk=2Trading=3 |
| joining(", ") | separate with delimiter | Risk=2, Trading=3 |
| joining(", ", "[", "]") | delimiter plus wrap | [Risk=2, Trading=3] |

JAVA
import java.util.List;
import java.util.stream.Collectors;

List<String> parts = List.of("Risk=2", "Trading=3");
System.out.println(parts.stream().collect(Collectors.joining()));          // Risk=2Trading=3
System.out.println(parts.stream().collect(Collectors.joining(", ")));       // Risk=2, Trading=3
System.out.println(parts.stream().collect(Collectors.joining(", ", "[", "]"))); // [Risk=2, Trading=3]

How do you produce the Map in the first place?

The count-per-department map typically comes from groupingBy with a counting downstream, then you format it. This shows the full path from raw employees to a one-line summary.

JAVA
package com.bank.report;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CountThenFormat {
    record Employee(String name, String department) {}

    public static void main(String[] args) {
        List<Employee> staff = List.of(
            new Employee("Rajesh", "Trading"),
            new Employee("Priya", "Risk"),
            new Employee("Amit", "Trading"),
            new Employee("Sunita", "Risk"),
            new Employee("Vikram", "Trading"));

        Map<String, Long> counts = staff.stream()
            .collect(Collectors.groupingBy(Employee::department, Collectors.counting()));

        String summary = counts.entrySet().stream()
            .sorted(Map.Entry.comparingByKey())
            .map(e -> e.getKey() + "=" + e.getValue())
            .collect(Collectors.joining(", ", "[", "]"));

        System.out.println(summary);
    }
}
TEXT
[Risk=2, Trading=3]

How do you sort by count instead of by department?

Use Map.Entry.comparingByValue() to order by count, and add .reversed() for descending, so the busiest department comes first.

JAVA
import java.util.Map;
import java.util.stream.Collectors;

Map<String, Long> counts = Map.of("Trading", 3L, "Risk", 2L, "Ops", 5L);

String byCountDesc = counts.entrySet().stream()
    .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
    .map(e -> e.getKey() + "=" + e.getValue())
    .collect(Collectors.joining(", ", "[", "]"));

System.out.println(byCountDesc); // [Ops=5, Trading=3, Risk=2]

What are common mistakes with joining a map?

The most common mistake is trying to join a stream of entries directly. joining needs a Stream, so you must map entries to strings first.

JAVA
// WRONG: joining cannot accept a stream of Map.Entry; this does not compile
// counts.entrySet().stream().collect(Collectors.joining(", ", "[", "]"));

// WRONG: forgetting to sort gives nondeterministic order from a HashMap
// counts.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue())
//        .collect(Collectors.joining(", ", "[", "]")); // order may vary run to run
JAVA
// CORRECT: map entries to strings, sort for determinism, then join
String summary = counts.entrySet().stream()
    .sorted(Map.Entry.comparingByKey())
    .map(e -> e.getKey() + "=" + e.getValue())
    .collect(Collectors.joining(", ", "[", "]"));
System.out.println(summary);
TEXT
[Ops=5, Risk=2, Trading=3]

What happens with an empty map?

On an empty stream, joining with a prefix and suffix emits just those: an empty-brackets string. This is correct and avoids null or a stray delimiter.

JAVA
package com.bank.report;

import java.util.Map;
import java.util.stream.Collectors;

public class EmptySummary {
    public static void main(String[] args) {
        Map<String, Long> empty = Map.of();
        String summary = empty.entrySet().stream()
            .sorted(Map.Entry.comparingByKey())
            .map(e -> e.getKey() + "=" + e.getValue())
            .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(summary); // []
    }
}
TEXT
[]

Why is joining efficient compared to manual string concatenation?

joining uses a StringJoiner backed by a StringBuilder, so it appends pieces without creating a new string for every concatenation. Building the same summary with += in a loop creates many intermediate strings and is slower for large maps.

JAVA
// WRONG for large data: quadratic string building
String s = "[";
boolean first = true;
for (var e : counts.entrySet()) {
    if (!first) s += ", ";
    s += e.getKey() + "=" + e.getValue(); // new String each iteration
    first = false;
}
s += "]";

// CORRECT: joining does it in one StringBuilder pass
String better = counts.entrySet().stream()
    .sorted(Map.Entry.comparingByKey())
    .map(e -> e.getKey() + "=" + e.getValue())
    .collect(java.util.stream.Collectors.joining(", ", "[", "]"));

How do you stream keys or values only?

If the summary needs only the departments or only the counts, stream keySet() or values() and map them to strings before joining.

JAVA
import java.util.Map;
import java.util.stream.Collectors;

Map<String, Long> counts = Map.of("Trading", 3L, "Risk", 2L);

String depts = counts.keySet().stream()
    .sorted()
    .collect(Collectors.joining(", ", "[", "]"));
System.out.println(depts); // [Risk, Trading]

String totals = counts.values().stream()
    .map(String::valueOf)
    .collect(Collectors.joining(", ", "[", "]"));
System.out.println(totals); // order depends; map to string first

What are common interview follow-up questions?

Expect: Why can you not call stream() on a Map? Answer: a Map is not a Collection; stream its entrySet, keySet, or values. How do you make the summary deterministic? Answer: sort the entry stream by key before mapping and joining. Why must you map entries to strings before joining? Answer: joining requires a Stream of CharSequence. What does joining return for an empty stream? Answer: just the prefix and suffix, so [] with those brackets. Why is joining faster than +=? Answer: it uses a StringBuilder internally instead of allocating a new string per concatenation.

Visual Overview

DIAGRAM
MAP IS NOT A COLLECTION: STREAM A VIEW

   Map<String,Long> counts
   ┌───────────────────────────┐
   │ Trading = 3                │
   │ Risk    = 2                │
   └───────────────────────────┘
        │ entrySet()     │ keySet()      │ values()
        ▼                ▼               ▼
   Stream<Entry>     Stream<String>   Stream<Long>


PIPELINE TO A FORMATTED SUMMARY

   entrySet().stream()
        │
        ▼
   sorted(comparingByKey)     ──▶  Risk=2 before Trading=3 (deterministic)
        │
        ▼
   map(e -> key + "=" + value) ──▶  "Risk=2", "Trading=3"  (Stream<String>)
        │
        ▼
   collect(joining(", ", "[", "]"))
        │
        ▼
   ┌──────────────────────┐
   │ [Risk=2, Trading=3]   │
   └──────────────────────┘


JOINING WRAPS EVEN WHEN EMPTY

   empty stream + joining(", ", "[", "]")  ──▶  "[]"

Key Takeaway

A Map is not itself a Collection, so it has no stream method, and the first thing you do is obtain a stream from one of its views: entrySet for key-value pairs, keySet for keys, or values for values. To build a formatted summary from a count-per-department map you stream the entrySet, sort it by key so the output is deterministic, map each entry to text like department equals count, and collect with Collectors.joining.

The joining collector with a delimiter, prefix, and suffix concatenates the pieces with the delimiter between them and wraps the whole result with the prefix and suffix, so you get a tidy line like open bracket, Risk equals two, comma space, Trading equals three, close bracket. In our work at the National Bank of India, Priya used exactly this to log a one-line, sorted, bracketed headcount summary at the end of each batch so it stood out in the logs.

The points worth stressing are that joining operates on a stream of character sequences, so you must map the entries to strings before collecting; trying to join a stream of entries directly is a common mistake. joining is also efficient because it uses a StringBuilder internally rather than allocating a new string per concatenation.

Finally, rely on its correct empty-stream behaviour, where it emits just the prefix and suffix, giving an empty-brackets string, and sort the entries first so the summary is stable regardless of the map's internal ordering. The same pattern sorts by value with comparingByValue when you want the busiest department first.

Remember one sentence: stream the entrySet, sort it, map each entry to text, then joining with a delimiter, prefix, and suffix stitches the whole summary into one bracketed line.

---

Share
Use ← → arrow keys to navigate