distinct()
Remove duplicate elements by equals and hashCode while keeping the first occurrence, and de-duplicate by a key when whole objects differ.
Stream<T> distinct()- Added in Java Java 8
Return value
Stream<T>A new stream with duplicates removed according to Object.equals. For ordered streams the first occurrence of each element is kept and encounter order is preserved.
Errors
Nothing directly. Exceptions from equals or hashCode surface when the terminal operation runs.
distinct() drops elements that are equal to one already seen. Equality is decided by equals and hashCode, the same rules a HashSet uses. It takes no arguments, which is both its strength and its main limitation.
Order is kept
On an ordered stream (a List, an array, Stream.of) distinct keeps the first occurrence of each value and preserves encounter order. That makes it a good fit when order matters, for example de-duplicating tags while keeping the order the user typed them. new HashSet<>(list) removes duplicates too but loses the order; new LinkedHashSet<>(list) keeps it.
Equality is the whole story
- Records get
equalsandhashCodefrom all their components, so two records with the same values are duplicates. - Ordinary classes without overridden
equalsuse identity: twonew Customer("c1")objects are different, anddistinctkeeps both. - Strings compare case-sensitively. Normalise first, for example
map(String::toLowerCase)for email addresses, then calldistinct. - Mutable keys: if an element changes after being seen, its hash changes and the de-duplication becomes unreliable.
Distinct by a key
There is no distinctBy(keyExtractor) on Stream. When you want one element per user ID, per SKU or per email, collect into a map keyed by that field and keep the first value:
events.stream()
.collect(Collectors.toMap(Event::userId, e -> e, (first, later) -> first, LinkedHashMap::new))
.values();The merge function decides which duplicate wins and LinkedHashMap keeps the order. Swap in (first, later) -> later to keep the most recent.
Cost
distinct is stateful: it remembers every distinct element it has seen, so memory grows with the number of unique values. That is fine for a page of results; on a stream of millions of rows, de-duplicate in the query with SELECT DISTINCT or a unique constraint. On parallel ordered streams, keeping first-occurrence order adds buffering; if order does not matter, unordered().distinct() is cheaper.
Example
When to use it
- Remove repeated tags from a blog post request while keeping the order the author typed them.
- Send one notification per recipient after normalising email addresses to lower case.
- Build the list of distinct product IDs in a batch of orders before loading the products in one query.
Frequently asked questions
How do I remove duplicates from a list with streams?
list.stream().distinct().toList() keeps the first occurrence of each element in the original order. Equality is decided by equals and hashCode, so make sure your element type implements them; records do automatically.
How do I make distinct work on one property of an object?
Stream has no distinctBy. Collect into a map keyed by the property with a merge function that keeps the first value, using LinkedHashMap to keep order: collect(Collectors.toMap(T::key, t -> t, (a, b) -> a, LinkedHashMap::new)).values().
Why does distinct not remove my duplicate objects?
Your class probably does not override equals and hashCode, so each object is only equal to itself. Override both methods, or turn the class into a record, which generates them from its components.
More examples
Classes without equals are never duplicates
The class inherits identity-based equals from Object, so two objects with the same ID are still different. The record generates equals and hashCode from its components, so distinct treats the two as one.
class Customer {
final String id;
Customer(String id) {
this.id = id;
}
}
record CustomerRef(String id) {}
void main() {
long classes = Stream.of(new Customer("c1"), new Customer("c1")).distinct().count();
IO.println(classes);
long records = Stream.of(new CustomerRef("c1"), new CustomerRef("c1")).distinct().count();
IO.println(records);
}
// Output:
// 2
// 1One element per key, keeping the first
The two u1 events are not equal as whole records, so distinct would keep both. Keying a LinkedHashMap by userId with a keep-first merge function gives distinct-by-key and preserves the original order.
record Event(String userId, String action) {}
void main() {
List<Event> events = List.of(
new Event("u1", "login"),
new Event("u2", "login"),
new Event("u1", "logout"),
new Event("u3", "login"));
Collection<Event> firstPerUser = events.stream()
.collect(Collectors.toMap(Event::userId, e -> e, (first, later) -> first, LinkedHashMap::new))
.values();
IO.println(firstPerUser);
}
// Output:
// [Event[userId=u1, action=login], Event[userId=u2, action=login], Event[userId=u3, action=login]]distinct keeps order, a TreeSet sorts
distinct and LinkedHashSet both keep first-seen order, while a TreeSet sorts. Choose based on whether the caller cares about the original order, such as the order items were added to a basket.
void main() {
List<Integer> productIds = List.of(7, 3, 7, 1, 3, 9);
IO.println(productIds.stream().distinct().toList());
IO.println(new TreeSet<>(productIds));
IO.println(new LinkedHashSet<>(productIds));
IO.println(productIds.stream().distinct().count());
}
// Output:
// [7, 3, 1, 9]
// [1, 3, 7, 9]
// [7, 3, 1, 9]
// 4
Discussion