Back to Stream Practice: Employee Analysis
Q8Question 8 of 43

Remove duplicates from a list: by value and by object field using streams

Key Concepts
  • distinct() removes duplicates using equals() and hashCode(), works directly for Strings and primitives
  • For dedup by a specific field, use the distinctByKey utility pattern with ConcurrentHashMap.newKeySet()
  • Alternative: toMap(keyExtractor, identity, mergeFunction).values() deduplicates via Map's unique-key property
  • distinct() preserves first-occurrence order in ordered streams and uses O(n) memory internally
  • ConcurrentHashMap.newKeySet() in distinctByKey makes it safe for parallel streams
  • For composite key dedup, concatenate fields: distinctByKey(e -> e.name() + "|" + e.department())

Definition

Use distinct() for value-based dedup via equals/hashCode, and the distinctByKey utility pattern with ConcurrentHashMap for field-based dedup without modifying equals.
  • distinct() removes duplicates based on the object's equals() and hashCode() methods, using an internal HashSet to track seen elements.
  • distinctByKey pattern: a reusable Predicate backed by ConcurrentHashMap.newKeySet() where seen.add(keyExtractor.apply(t)) returns true for new keys and false for duplicates.
  • toMap alternative: toMap(keyExtractor, identity, (first, second) -> first).values() collects into a Map (inherently unique keys) with a merge function keeping the first occurrence.
  • distinct() alone only works if equals/hashCode are defined on the desired dedup field. For dedup by a specific field without modifying equals, use the distinctByKey pattern.
  • Both approaches preserve first-occurrence order in ordered streams.

National Bank of India receives trade confirmations from both BSE and NSE for the same trades. Some confirmations are duplicates (same trade ID from both exchanges). The bank needs to deduplicate: by trade ID (keep only the first confirmation for each trade), or by trade value (keep only unique amounts). The distinctByKey pattern is like giving the dedup clerk a stamp pad: stamp each trade ID on a sheet, and if the ID is already there, discard the duplicate.

Understanding the Concepts

What is the difference between dedup by value and dedup by field?

Dedup by value means two objects are duplicates if they are equals to each other. Dedup by field means two objects are duplicates if they share the same value for a specific field, even if other fields differ.

How do you remove duplicates by value?

distinct() uses equals() and hashCode() to identify duplicates.

JAVA
package com.bank.service;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class DedupDemo {
    public static void main(String[] args) {
        // BY VALUE: distinct() uses equals + hashCode
        List<String> symbols = List.of(
            "RELIANCE", "TCS", "RELIANCE", "HDFC", "TCS");

        List<String> unique = symbols.stream()
            .distinct()
            .toList();

        System.out.println("Unique symbols: " + unique);
    }
}
TEXT
Unique symbols: [RELIANCE, TCS, HDFC]

How do you remove duplicates by a specific field?

Pattern 1: Use the distinctByKey utility predicate.

JAVA
// Reusable distinctByKey utility
public static <T> Predicate<T> distinctByKey(
        Function<? super T, ?> keyExtractor) {
    Set<Object> seen = ConcurrentHashMap.newKeySet();
    return t -> seen.add(keyExtractor.apply(t));
}
JAVA
record Employee(String id, String name, String department) {}

List<Employee> employees = List.of(
    new Employee("E001", "Rajesh",  "TRADING"),
    new Employee("E002", "Priya",   "RISK"),
    new Employee("E001", "Rajesh",  "OPERATIONS"),  // duplicate ID
    new Employee("E003", "Amit",    "TRADING"),
    new Employee("E002", "Priya",   "COMPLIANCE")   // duplicate ID
);

// Deduplicate by employee ID
List<Employee> uniqueById = employees.stream()
    .filter(distinctByKey(Employee::id))
    .toList();

uniqueById.forEach(e ->
    System.out.println(e.id() + ": " + e.name() + " " + e.department()));
TEXT
E001: Rajesh TRADING
E002: Priya RISK
E003: Amit TRADING

The first occurrence of each ID is kept. The duplicates with different departments are discarded.

How does the toMap alternative work?

JAVA
// Pattern 2: toMap with merge function
List<Employee> uniqueViaMap = employees.stream()
    .collect(Collectors.toMap(
        Employee::id,
        e -> e,
        (first, second) -> first))  // keep first occurrence
    .values().stream().toList();

The Map's unique-key property naturally deduplicates. The merge function (first, second) -> first determines which duplicate to keep.

How do you deduplicate by multiple fields?

Concatenate the fields into a composite key.

JAVA
// Deduplicate by name + department combination
List<Employee> uniqueByComposite = employees.stream()
    .filter(distinctByKey(e -> e.name() + "|" + e.department()))
    .toList();

How is distinct() different from distinctByKey?

| Feature | distinct() | distinctByKey(keyExtractor) |
|---------|-------------|------------------------------|
| Dedup criterion | equals() + hashCode() | Extracted key field |
| Requires | Correct equals/hashCode on the object | Only equals/hashCode on the KEY type |
| Modifies class? | Depends on existing equals | No modification needed |
| Thread safety | Internal HashSet | ConcurrentHashMap (parallel-safe) |
| Reusable? | Built-in | Utility method you write once |

What are common mistakes?

WRONG: Assuming distinct() deduplicates by a specific field without proper equals/hashCode.

JAVA
// WRONG: distinct() uses object identity unless equals/hashCode are overridden
// Two Employee objects with the same ID but different departments are NOT equal
// unless equals is based on ID
List<Employee> wrong = employees.stream()
    .distinct()  // does nothing if equals compares all fields
    .toList();

CORRECT: Use distinctByKey for field-based dedup.

JAVA
// CORRECT: explicit field-based dedup
List<Employee> correct = employees.stream()
    .filter(distinctByKey(Employee::id))
    .toList();

What about parallel stream safety?

The distinctByKey utility uses ConcurrentHashMap.newKeySet(), which is thread-safe. Multiple threads can call seen.add() concurrently without data races. This makes it safe for parallel streams.

JAVA
// Safe for parallel streams
List<Employee> parallelDedup = employees.parallelStream()
    .filter(distinctByKey(Employee::id))
    .toList();

What should a senior engineer know?

  • distinct() uses an internal LinkedHashSet in the stream implementation, giving O(n) memory usage.
  • The distinctByKey pattern is stateful: it captures a mutable Set in the predicate closure. This is acceptable because ConcurrentHashMap is thread-safe, but it means the predicate cannot be reused across multiple stream pipelines.
  • For very large datasets, consider whether dedup should happen at the database level (SQL DISTINCT or GROUP BY) rather than in memory.
  • The toMap approach does not preserve the original order unless you use LinkedHashMap::new as the fourth argument.

Visual Overview

DIAGRAM
  employees stream
  [E001-TRADING, E002-RISK, E001-OPS, E003-TRADING, E002-COMPLIANCE]
       │
       ▼
  filter(distinctByKey(Employee::id))
       │
  ┌────┴────────────────────────────────────┐
  │ seen = ConcurrentHashMap.newKeySet()    │
  │                                         │
  │ E001 → seen.add("E001") = true  ✓ KEEP │
  │ E002 → seen.add("E002") = true  ✓ KEEP │
  │ E001 → seen.add("E001") = false ✗ SKIP │
  │ E003 → seen.add("E003") = true  ✓ KEEP │
  │ E002 → seen.add("E002") = false ✗ SKIP │
  └────┬────────────────────────────────────┘
       │
       ▼
  [E001-TRADING, E002-RISK, E003-TRADING]

Key Takeaway

Removing duplicates by value uses distinct(), which relies on equals() and hashCode(). For strings and primitives, this works out of the box. For custom objects where you want to deduplicate by a specific field without modifying equals, use the distinctByKey utility pattern: a Predicate backed by ConcurrentHashMap.newKeySet() where seen.add(keyExtractor.apply(t)) returns true for new keys and false for duplicates.

The alternative approach uses Collectors.toMap(keyExtractor, identity, mergeFunction) which inherently deduplicates because Map keys are unique. The merge function determines which duplicate to keep.

At National Bank of India, seven trade confirmations from BSE and NSE are deduplicated by trade ID (five unique trades), by symbol (three unique symbols), and by composite key (symbol + side). The distinctByKey utility is the most flexible because it accepts any key extractor function.

Remember one sentence: distinct works by equals/hashCode, but distinctByKey with ConcurrentHashMap lets you deduplicate by any field without modifying the class.

---

Share
Use ← → arrow keys to navigate