Definition
- **string.chars()** returns an `IntStream` of character codes. - **mapToObj(c -> (char) c)** boxes each into a `Character`. - **groupingBy(c -> c, LinkedHashMap::new, counting())** counts preserving insertion order. - **LinkedHashMap** is mandatory: HashMap would lose the order and 'first' would be arbitrary. - **findFirst()** on the ordered entries returns `Optional<Character>` for the first non-repeating char.
Priya at the National Bank of India scans a reference code letter by letter, tallying how often each letter appears while remembering the order she first saw them. The first letter with a tally of one is the answer.
Understanding the Concepts
How do you find the first non-repeating character?
Count character frequencies in a LinkedHashMap (preserves first-seen order), then find the first entry with count 1.
package com.bank.core;
import java.util.*;
import java.util.stream.Collectors;
public class FirstNonRepeating {
public static void main(String[] args) {
String input = "swiss";
Optional<Character> result = input.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
c -> c,
LinkedHashMap::new,
Collectors.counting()))
.entrySet().stream()
.filter(e -> e.getValue() == 1L)
.map(Map.Entry::getKey)
.findFirst();
result.ifPresentOrElse(
ch -> System.out.println("First non-repeating: '" + ch + "'"),
() -> System.out.println("All characters repeat"));
}
}
First non-repeating: 'w'
Why is LinkedHashMap mandatory?
HashMap does not preserve insertion order, so the 'first' non-repeating character would be arbitrary.
| Map type | Preserves first-seen order? | Correct for this problem? |
|---|---|---|
| LinkedHashMap | Yes | Yes |
| HashMap | No | No |
| TreeMap | No (sorts by key) | No |
// WRONG: HashMap loses order
Map<Character, Long> unordered = input.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(c -> c, Collectors.counting()));
// Iterating this map gives an arbitrary character with count 1
// CORRECT: LinkedHashMap preserves first-seen order
Map<Character, Long> ordered = input.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
c -> c, LinkedHashMap::new, Collectors.counting()));
What about case sensitivity?
// Case-insensitive: convert to lowercase first
String lower = input.toLowerCase();
Optional<Character> caseInsensitive = lower.chars()
.mapToObj(c -> (char) c)
.collect(Collectors.groupingBy(
c -> c, LinkedHashMap::new, Collectors.counting()))
.entrySet().stream()
.filter(e -> e.getValue() == 1L)
.map(Map.Entry::getKey)
.findFirst();
What about strings where all characters repeat?
The result is Optional.empty(), correctly representing 'no unique character found'.
String allRepeat = "aabbcc";
// Result: Optional.empty()
What are common mistakes?
Mistake: using HashMap instead of LinkedHashMap.
// WRONG: HashMap gives an arbitrary character, not the first one
GroupingBy(c -> c, Collectors.counting()) // default is HashMap
// CORRECT: explicitly specify LinkedHashMap
groupingBy(c -> c, LinkedHashMap::new, Collectors.counting())
What about Unicode characters beyond the BMP?
chars() iterates individual char values. For emoji or rare scripts, use codePoints() instead.
// For surrogate pairs (emoji, etc.):
String emoji = "hello🌍world🌍";
emoji.codePoints()
.mapToObj(cp -> new String(Character.toChars(cp)))
.collect(Collectors.groupingBy(
s -> s, LinkedHashMap::new, Collectors.counting()));
What should a senior engineer know?
- LinkedHashMap is mandatory for correctness; this is the key insight interviewers test.
- The algorithm is O(n): one pass to count, one pass to scan. Space is O(k) where k is the alphabet size.
- Return Optional to honestly represent the case where no unique character exists.
- Decide case sensitivity and whitespace handling up front; both change the answer.
What are common interview follow-up questions?
- "How do you find the first repeating character?" Filter for count > 1 instead of count == 1.
- "Can you solve it in one pass?" Yes, but it requires a manual linked structure, not a stream-friendly approach.
- "How do you find the last non-repeating character?" Use reduce instead of findFirst to get the last match.
Visual Overview
String: "swiss"
│
│ chars() → mapToObj
▼
Stream<Character> ['s', 'w', 'i', 's', 's']
│
│ groupingBy(c, LinkedHashMap, counting())
▼
LinkedHashMap (preserves first-seen order):
's' → 3
'w' → 1
'i' → 1
│
│ filter(count == 1) → findFirst()
▼
Optional['w'] ← first non-repeating character
Key Takeaway
To find the first non-repeating character, stream the string's characters, count them with groupingBy using a LinkedHashMap to preserve first-seen order, then find the first entry with count 1. LinkedHashMap is mandatory: without it, 'first' would be arbitrary because HashMap has no ordering guarantee.
For 'swiss', the LinkedHashMap preserves the order s, w, i. The first entry with count 1 is 'w'. For all-repeating strings like 'aabbcc', the result is correctly Optional.empty().
At the National Bank of India, this pattern was used in a reference-code validator. The algorithm is O(n) with one counting pass and one scan, and returning Optional honestly represents the no-unique-character case.
Remember one sentence: LinkedHashMap preserves first-seen order, which is what makes 'first non-repeating' meaningful, and always decide case sensitivity up front.
---