Definition
- distinct() removes duplicates based on the object's
equals()andhashCode()methods, using an internalHashSetto track seen elements. - distinctByKey pattern: a reusable
Predicatebacked byConcurrentHashMap.newKeySet()whereseen.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 ifequals/hashCodeare defined on the desired dedup field. For dedup by a specific field without modifyingequals, 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.
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);
}
}
Unique symbols: [RELIANCE, TCS, HDFC]
How do you remove duplicates by a specific field?
Pattern 1: Use the distinctByKey utility predicate.
// 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));
}
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()));
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?
// 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.
// 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.
// 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.
// 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.
// Safe for parallel streams
List<Employee> parallelDedup = employees.parallelStream()
.filter(distinctByKey(Employee::id))
.toList();
What should a senior engineer know?
distinct()uses an internalLinkedHashSetin the stream implementation, giving O(n) memory usage.- The
distinctByKeypattern is stateful: it captures a mutableSetin the predicate closure. This is acceptable becauseConcurrentHashMapis 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
DISTINCTorGROUP BY) rather than in memory. - The
toMapapproach does not preserve the original order unless you useLinkedHashMap::newas the fourth argument.
Visual Overview
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.
---