ZetCode

Java Stream distinct

最后修改时间 2025 年 5 月 8 日

本文演示了如何使用 Java Stream distinct 方法从流中删除重复元素。

distinct 方法是 Java Streams 中的一个中间操作,它过滤掉重复元素,确保流中只剩下唯一值。它基于元素的 equals 方法确定唯一性。

对于有序流,distinct 保持原始的遇到顺序,保留元素出现的顺序。 相反,对于无序流,删除重复项可以通过减少不必要的顺序跟踪开销来提高性能。

基本 distinct 语法

distinct 方法提供了一种简单的方法来从流中删除重复元素,确保只保留唯一值。

Stream<T> distinct()

此操作依赖于 equals 方法来比较元素并识别重复项。 为了确保正确的行为,流元素应该适当地实现 equalshashCode 方法。 不正确的实现可能会在过滤唯一元素时导致意外的结果。

distinct 的内部工作原理

Java Streams 中的 distinct 方法不是基于哈希或键值存储(如 HashMap)。 而是执行有状态过滤,以确保流中仅保留由其 equals 方法确定的唯一元素。

功能StreamHashMap
目的动态处理和转换数据高效存储键值对
数据存储存储元素在哈希结构中存储元素
唯一性逻辑distinct 中使用 equals使用哈希进行快速查找
性能对于大型数据集可能较慢针对 O(1) 键查找进行了优化

在内部,distinct 通过使用 LinkedHashSet 跟踪先前看到的元素来维护有状态的过滤器。 当处理流时,针对已经看到的元素检查每个元素的相等性。 如果元素是唯一的(根据 equals),则将其传递到下游;否则,将其过滤掉。 这种方法保留了遇到顺序,但对于非常大的数据集来说,效率可能不如哈希。

与使用哈希提供快速 O(1) 查找的 HashMap 不同,流中的 distinct 不会索引元素以进行快速访问。 而是顺序比较每个元素,这会影响大型流的性能。

从原始值中删除重复项

distinct 方法可以与原始值流一起使用,以消除重复项并仅保留唯一元素。

Main.java
void main() {

    Stream.of(2, 5, 3, 2, 5, 7, 3, 8)
          .distinct()
          .forEach(System.out::println);
}

此示例从流中删除重复的整数。 distinct 操作保留每个唯一数字的首次出现。

$ java Main.java
2
5
3
7
8

删除重复的字符串

distinct 方法也可以应用于字符串流,以过滤掉重复的值并仅保留唯一的字符串。

Main.java
void main() {

    Stream.of("apple", "orange", "apple", "banana", "orange")
          .distinct()
          .forEach(System.out::println);
}

此示例从流中删除重复的字符串。 字符串比较区分大小写,因此 "Apple" 和 "apple" 将被视为不同的。

$ java Main.java
apple
orange
banana

具有 equals/hashCode 的自定义对象

当将 distinct 与自定义对象一起使用时,重要的是对象正确实现 equalshashCode 以确保正确识别重复项。

Main.java
record Person(String name, int age) {
}

void main() {

    Stream.of(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Alice", 30),
            new Person("Charlie", 35),
            new Person("Bob", 25)
        )
        .distinct()
        .forEach(p -> System.out.println(p.name() + " - " + p.age()));
}

此示例删除重复的 Person 对象。 记录会自动实现正确的equalshashCode 方法,基于它们的组件。

$ java Main.java
Alice - 30
Bob - 25
Charlie - 35

没有正确 equals/hashCode 的自定义对象

如果自定义对象未正确实现 equalshashCode,则 distinct 方法可能无法按预期识别重复项。

Main.java
class Product {

    String name;
    double price;
    
    Product(String name, double price) {
        this.name = name;
        this.price = price;
    }
    
    // No equals/hashCode implementation
}

void main() {

    Stream.of(
            new Product("Laptop", 999.99),
            new Product("Phone", 699.99),
            new Product("Laptop", 999.99)
        )
        .distinct()
        .forEach(p -> System.out.println(p.name + " - " + p.price));
}

此示例表明,如果没有正确的 equalshashCode 方法,distinct 将无法按预期工作,会将具有相同值的对象视为不同的对象。

$ java Main.java
Laptop - 999.99
Phone - 699.99
Laptop - 999.99

与其他操作结合使用

distinct 方法可以与其他流操作(例如过滤和映射)结合使用,以创建更复杂的数据处理管道。

Main.java
void main() {

    Stream.of("apple", "banana", "apple", "orange", "banana", "kiwi")
          .filter(s -> s.length() > 4)
          .distinct()
          .map(String::toUpperCase)
          .forEach(System.out::println);
}

此示例过滤长水果,删除重复项,并转换为大写,展示了 distinct 如何与其他操作组合使用。

$ java Main.java
BANANA
ORANGE

Distinct 与嵌套集合

在将嵌套集合展平为单个流后,distinct 方法可用于删除重复项。

Main.java
void main() {

    List<List<String>> nestedLists = List.of(
        List.of("a", "b", "c"),
        List.of("b", "c", "d"),
        List.of("c", "d", "e")
    );
    
    nestedLists.stream()
              .flatMap(List::stream)
              .distinct()
              .forEach(System.out::println);
}

此示例展平嵌套列表,然后删除重复元素,演示了 distinct 的常见用例。

$ java Main.java
a
b
c
d
e

文本文件中的不同单词

distinct 方法可用于从文本文件中提取所有唯一的单词,忽略大小写和标点符号,这对于构建词汇表列表或分析文档中的唯一单词等任务很有用。

thermopylae.txt
The Battle of Thermopylae was fought between an alliance of Greek city-states,
led by King Leonidas of Sparta, and the Persian Empire of Xerxes I over the
course of three days, during the second Persian invasion of Greece.

此文件包含对温泉关战役的简要描述。 我们可以使用 distinct 方法从该文本文件中提取所有唯一的单词,忽略大小写和标点符号。

Main.java
void main() throws IOException {

    Path path = Paths.get("thermopylae.txt");

    Files.lines(path)
        .flatMap(line -> Arrays.stream(line.split("\\W+")))
        .map(String::toLowerCase)
        .filter(s -> !s.isEmpty())
        .distinct()
        .forEach(System.out::println);
}

此示例从文件中读取行,将它们拆分为单词,将它们规范化为小写,删除空字符串,并打印所有唯一的单词。 split("\\W+") 正则表达式根据任何非单词字符进行拆分,从而有效地删除标点符号。

来源

Java Stream distinct 文档

在本文中,我们探讨了 Java Stream distinct 方法。 它提供了一种有效的方法来从流中删除重复元素,但需要正确实现自定义对象的 equals 和代码。 了解 distinct 对于处理可能包含重复项的数据至关重要。

作者

我的名字是 Jan Bodnar,我是一位充满激情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有Java教程