The distinct() operation is an intermediate operation in the Java Stream API that removes duplicate elements from a stream. It is useful whenever a data source may contain repeated values but the application needs each value only once.
For example, imagine collecting programming skills from several employees. Multiple employees may know Java, SQL, or Docker. If you want a list of unique skills, distinct() provides a clean way to eliminate repeated values without manually maintaining a separate collection.
Why Do We Need distinct()?
Real-world data frequently contains duplicates. A database query may return repeated categories, users may submit the same value more than once, or multiple objects may refer to the same logical item. Before displaying, counting, or processing such data, duplicate removal may be necessary.
import java.util.List;
public class DistinctExample {
public static void main(String[] args) {
List<String> languages = List.of(
"Java",
"Python",
"Java",
"C++",
"Python"
);
languages.stream()
.distinct()
.forEach(System.out::println);
}
}
The resulting stream contains each language only once.
Important: distinct() does not simply remove adjacent duplicates. It identifies duplicate elements throughout the stream according to equality rules.
Syntax of distinct()
The syntax is simple:
stream.distinct()
Unlike sorted(), filter(), or map(), distinct() does not require a parameter. Java determines whether elements are duplicates using their equality semantics.
| Property | Details |
|---|---|
| Operation | distinct() |
| Type | Intermediate operation |
| Purpose | Remove duplicate elements |
| Duplicate rule | Based on equals() and hashCode() |
| Parameters | None |
| Source collection | Not modified directly |
distinct() with Numbers
With numeric wrapper objects such as Integer, duplicate values are removed according to their equality behavior.
import java.util.List;
public class NumberDistinct {
public static void main(String[] args) {
List<Integer> numbers = List.of(
10, 20, 10, 30, 20, 40, 10
);
List<Integer> uniqueNumbers = numbers.stream()
.distinct()
.toList();
System.out.println(uniqueNumbers);
}
}
The result contains 10, 20, 30, 40. Each repeated value appears only once.
distinct() Preserves Encounter Order
For an ordered stream, distinct() keeps the first occurrence of each value and preserves the stream's encounter order.
List<String> names = List.of(
"Rahul",
"Amit",
"Rahul",
"Priya",
"Amit"
);
List<String> uniqueNames = names.stream()
.distinct()
.toList();
System.out.println(uniqueNames);
The result is ordered according to the first appearance of each name.
Remember: distinct() removes repeated values, but it does not randomly rearrange an ordered stream. The first encountered occurrence is retained.
distinct() with filter()
The order of stream operations matters. You can filter values first and then remove duplicates.
List<Integer> numbers = List.of(
10, 15, 20, 15, 25, 20, 30
);
List<Integer> result = numbers.stream()
.filter(number -> number > 15)
.distinct()
.toList();
System.out.println(result);
Only values greater than 15 reach distinct(), so duplicate tracking is performed on a smaller set of elements.
distinct() Before filter()
You can also place distinct() before filtering.
List<Integer> result = numbers.stream()
.distinct()
.filter(number -> number > 15)
.toList();
This can be useful when duplicate values should be eliminated before any further processing. However, when a filter removes many elements, filtering first may reduce the amount of work required by distinct().
distinct() with map()
You can map objects to a property and then remove duplicate property values.
import java.util.List;
public class EmployeeDepartments {
record Employee(
String name,
String department
) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Amit", "IT"),
new Employee("Priya", "HR"),
new Employee("Rahul", "IT"),
new Employee("Neha", "Finance"),
new Employee("Karan", "HR")
);
List<String> departments = employees.stream()
.map(Employee::department)
.distinct()
.toList();
System.out.println(departments);
}
}
Here, map() extracts department names first. Then distinct() removes repeated department values.
distinct() with flatMap()
A particularly useful combination is flatMap() followed by distinct(). This is common when nested collections contain repeated values.
import java.util.List;
public class UniqueSkills {
record Employee(
String name,
List<String> skills
) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Amit", List.of("Java", "SQL")),
new Employee("Priya", List.of("Java", "Docker")),
new Employee("Rahul", List.of("SQL", "Java"))
);
List<String> skills = employees.stream()
.flatMap(employee -> employee.skills().stream())
.distinct()
.toList();
System.out.println(skills);
}
}
First, flatMap() combines all employee skill lists into one stream. Then distinct() removes repeated skills.
distinct() with Objects
When working with custom objects, understanding equality is critical. Consider this example:
import java.util.List;
public class ObjectDistinct {
record Employee(
String name,
String department
) {}
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Amit", "IT"),
new Employee("Amit", "IT"),
new Employee("Priya", "HR")
);
employees.stream()
.distinct()
.forEach(System.out::println);
}
}
Because record automatically provides value-based equals() and hashCode() implementations, the two identical employee records are considered equal and only one remains.
How distinct() Determines Duplicates
For object streams, duplicate detection depends on equality. In practical terms, Java uses the object's equals() and hashCode() contract to determine whether values should be treated as the same.
employee1.equals(employee2)
If two objects represent the same logical value according to their equality contract, distinct() can treat them as duplicates.
Critical Concept: For custom classes, correct equals() and hashCode() implementations are essential when using operations such as distinct().
A Common Mistake with Custom Objects
Suppose a normal class does not override equals() and hashCode().
class Employee {
String name;
Employee(String name) {
this.name = name;
}
}
Two separate Employee objects with the same name may still be treated as different objects because the default equality behavior does not define them as equal based on their name.
Therefore, simply writing distinct() does not mean "remove objects with the same name." It means "remove elements considered equal according to their equality contract."
Removing Duplicates by One Property
A frequent requirement is to remove duplicate objects based on a particular field, such as employee ID, email address, or product code.
For example, if two employee objects have the same email address but different names, ordinary distinct() will only consider them duplicates if their equality implementation says so.
One clean approach is to map the objects to the property that defines uniqueness.
List<String> uniqueEmails = employees.stream()
.map(Employee::email)
.distinct()
.toList();
If you need the complete employee objects while applying uniqueness to one property, the stream pipeline usually needs a different strategy, such as collecting into a map keyed by that property.
distinct() and sorted()
The two operations can be combined when the requirement is to obtain unique values in sorted order.
List<Integer> result = numbers.stream()
.distinct()
.sorted()
.toList();
System.out.println(result);
The stream first removes duplicates and then sorts the remaining values.
The reverse order is also valid:
List<Integer> result = numbers.stream()
.sorted()
.distinct()
.toList();
Both can produce the same unique sorted values for an ordered sequential stream, but the first version may avoid sorting repeated values. In general, consider which operation can reduce the amount of work required by the next operation.
distinct() with count()
One useful pattern is counting unique values.
long uniqueCount = languages.stream()
.distinct()
.count();
System.out.println(uniqueCount);
This is useful for questions such as "How many different programming languages are represented?" or "How many unique customers placed orders?"
distinct() with anyMatch()
Not every problem requires distinct(). If you only need to know whether a condition is satisfied, duplicate removal may be unnecessary.
boolean containsJava = skills.stream()
.anyMatch(
skill ->
skill.equals("Java")
);
Sorting or deduplicating the entire stream would add unnecessary work when a short-circuiting operation can answer the question directly.
distinct() with Null Values
distinct() can handle repeated null values in a reference stream. Only one null element is retained.
import java.util.List;
public class NullDistinct {
public static void main(String[] args) {
List<String> values = List.of(
"Java",
"Spring",
"Java"
);
values.stream()
.distinct()
.forEach(System.out::println);
}
}
When a data source can contain null values, remember that other stream operations in the pipeline may have different null-handling requirements. The behavior of the complete pipeline should always be considered.
distinct() in Real-World Data Processing
Imagine an e-commerce system where many orders contain product categories. The application wants to display all categories represented in a customer's order history.
List<String> categories = orders.stream()
.flatMap(order -> order.items().stream())
.map(Item::category)
.distinct()
.toList();
This pipeline reads naturally from left to right: flatten order items, extract their categories, remove duplicates, and collect the result.
This is one of the strengths of the Stream API. Each operation expresses one small transformation, while the complete pipeline describes the business requirement clearly.
Performance Considerations
distinct() generally needs to remember previously encountered elements so that later duplicates can be identified. Therefore, it is not a completely stateless operation.
For large streams, this can require additional memory. The exact behavior and performance characteristics can also differ between sequential and parallel streams.
Performance Tip: Do not use distinct() simply because it is available. Use it when uniqueness is part of the actual requirement, and place filtering operations thoughtfully to reduce unnecessary processing.
Common Beginner Mistakes
- Thinking distinct() compares only neighboring elements: It removes duplicates throughout the stream, not just consecutive repetitions.
- Assuming distinct() uses a selected object property: Duplicate detection follows the object's equality contract.
- Forgetting equals() and hashCode(): Custom classes need correct equality semantics when logical duplicates must be recognized.
- Expecting the source collection to change: The stream pipeline does not directly modify the original collection.
- Using distinct() when a short-circuiting operation is enough: Operations such as anyMatch() may solve the requirement without tracking all unique values.
- Ignoring pipeline order: Filtering before distinct() can reduce the number of elements that need to be tracked when the filter is highly selective.
Best Practices
- Use distinct() when uniqueness is an explicit requirement.
- Understand the equals() and hashCode() contract for custom objects.
- Map objects to a property before distinct() when that property itself defines the required unique values.
- Filter irrelevant data before deduplication when doing so is logically correct and reduces the amount of data processed.
- Avoid unnecessary deduplication when a direct short-circuiting operation can answer the business question.
- Remember that distinct() may require additional memory for tracking encountered elements.
Interview Insights
Question: What does distinct() do in Java Streams?
Answer: distinct() removes duplicate elements from a stream according to the elements' equality semantics.
Question: Is distinct() an intermediate operation?
Answer: Yes. distinct() is an intermediate stream operation and is evaluated lazily as part of the pipeline.
Question: How does distinct() identify duplicates?
Answer: Duplicate detection is based on equality semantics, involving equals() and hashCode() for objects.
Question: Does distinct() preserve order?
Answer: For an ordered stream, it preserves encounter order and retains the first occurrence of each distinct value.
Question: Can distinct() remove duplicates based on one field?
Answer: Not directly. If uniqueness is based on a field, you can map to that field and call distinct(), or use an appropriate collection strategy when the complete objects must be retained.
Quick Revision
| Concept | Key Point |
|---|---|
| distinct() | Removes duplicate stream elements |
| Type | Intermediate operation |
| Parameters | None |
| Equality | Uses the element equality contract |
| Custom objects | Correct equals() and hashCode() are important |
| Order | Ordered streams retain encounter order |
| Common combination | flatMap() → map() → distinct() |
| Property uniqueness | Map to the property before distinct() when appropriate |
| Performance | May require memory to track encountered elements |
| Source | Original collection is not directly modified |
Final Takeaway
The distinct() operation is a small but powerful part of the Java Stream API. It removes repeated values while keeping the stream pipeline readable and expressive. The most important detail is that uniqueness is determined by equality semantics, so custom objects must define equals() and hashCode() correctly when logical duplicates need to be recognized. Once you combine distinct() thoughtfully with operations such as filter(), map(), and flatMap(), many real-world deduplication tasks become concise, readable, and maintainable.
