CodeGym /课程 /JAVA 25 SELF /Stream API 的子集操作: distinct<...

Stream API 的子集操作: distinctlimitskip

JAVA 25 SELF
第 30 级 , 课程 2
可用

1. 方法 distinct:删除重复项

在实际开发中,我们不仅需要过滤和转换数据,还常常需要只从集合中选出唯一元素、限制结果大小,或者相反,跳过前面若干个元素。例如:

  • 获取唯一的用户名列表。
  • 只取前 10 条记录用于页面展示。
  • 跳过前 5 个元素(例如实现分页——“从第 2 页开始显示”)。

针对这些需求,Stream API 提供了专门的方法:distinctlimitskip

distinct 如何工作?

方法 distinct() 会返回一个新流,其中所有重复元素都被删除。重复的判定由相应类的 equalshashCode 方法决定。

打个比方:你是一名钱币收藏者,希望主藏品中没有重复。所有重复的硬币都进入交换库。distinct() 正是能够把主集合中的重复元素筛掉的操作。

示例:唯一的用户名

List<String> names = List.of(
    "Alice", "Bob", "Alice", "Eva", "Bob", "Denis", "Gleb", "Eva"
);

获取唯一的姓名列表:

List<String> uniqueNames = names.stream()
    .distinct()
    .collect(Collectors.toList());

System.out.println(uniqueNames);
// 输出: [Alice, Bob, Eva, Denis, Gleb]

示例:唯一的用户 email

假设我们有一个 User 类:

public class User {
    String name;
    String email;

    // 构造函数、getter、toString() — 仅为方便
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    @Override
    public String toString() {
        return name + " <" + email + ">";
    }
}

包含重复 email 的用户列表:

List<User> users = List.of(
    new User("Alice", "alice@mail.com"),
    new User("Bob", "bob@mail.com"),
    new User("Eva", "eva@mail.com"),
    new User("Alice2", "alice@mail.com"), // email 重复!
    new User("Gleb", "gleb@mail.com"),
    new User("Eva2", "eva@mail.com")      // email 重复!
);

如果直接调用 users.stream().distinct(),重复项并不会被删除,因为 User 对象默认没有重写 equalshashCode。在这种情况下,distinct 只对“引用相同”的对象有效。

解决方案:重写 equalshashCode,让唯一性由 email 决定。

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return Objects.equals(email, user.email);
}

@Override
public int hashCode() {
    return Objects.hash(email);
}

现在:

List<User> uniqueUsers = users.stream()
    .distinct()
    .collect(Collectors.toList());

uniqueUsers.forEach(System.out::println);
// Alice <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>
// Gleb <gleb@mail.com>

重要:如果希望 distinct 按预期工作,在自定义类中一定要重写 equalshashCode

2. 方法 limit:限制元素数量

方法 limit(long maxSize) 会返回一个新流,包含原始流中不超过 maxSize 个的前缀元素。

类比:你走进一家甜品店,只想从 100 个蛋糕里试吃 3 个。调用 limit(3) —— 你就会拿到正好前三个,其余“今天就不吃了”。

示例:前三个名字

List<String> firstThree = names.stream()
    .limit(3)
    .collect(Collectors.toList());

System.out.println(firstThree);
// 输出: [Alice, Bob, Alice]

结合我们的示例应用:最新的前 3 位用户

List<User> firstUsers = users.stream()
    .limit(3)
    .collect(Collectors.toList());

firstUsers.forEach(System.out::println);
// Alice <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>

带排序的示例

可以与排序结合——例如,取 email 最短的前 2 个用户:

List<User> top2ShortEmail = users.stream()
    .sorted(Comparator.comparingInt(u -> u.email.length()))
    .limit(2)
    .collect(Collectors.toList());

top2ShortEmail.forEach(System.out::println);
// Bob <bob@mail.com>
// Eva <eva@mail.com>

3. 方法 skip:跳过前面元素

方法 skip(long n) 会返回一个新流,跳过原始流中的前 n 个元素。

示例:跳过前两个名字

List<String> afterTwo = names.stream()
    .skip(2)
    .collect(Collectors.toList());

System.out.println(afterTwo);
// 输出: [Alice, Eva, Bob, Denis, Gleb, Eva]

示例:分页(pagination)

网站上常见需求是“每页显示 3 个用户”。第二页需要跳过前 3 个,再取接下来的 3 个:

int pageSize = 3;
int pageNumber = 2; // 第 2 页

List<User> page = users.stream()
    .skip(pageSize * (pageNumber - 1))
    .limit(pageSize)
    .collect(Collectors.toList());

page.forEach(System.out::println);
// Alice2 <alice@mail.com>
// Gleb <gleb@mail.com>
// Eva2 <eva@mail.com>

4. 组合使用 distinctlimitskip

这些方法可以并且应该根据任务需要进行组合。

示例:从第 3 个开始,获取 2 个唯一的名字

List<String> result = names.stream()
    .distinct() // 去重: [Alice, Bob, Eva, Denis, Gleb]
    .skip(2)    // 跳过 Alice 和 Bob: [Eva, Denis, Gleb]
    .limit(2)   // 只取两个: [Eva, Denis]
    .collect(Collectors.toList());

System.out.println(result);
// 输出: [Eva, Denis]

带过滤的示例

假设要获取包含字母“a”的前 2 个唯一 email:

List<String> emails = users.stream()
    .map(user -> user.email)
    .filter(email -> email.contains("a"))
    .distinct()
    .limit(2)
    .collect(Collectors.toList());

System.out.println(emails);
// 输出: [alice@mail.com, eva@mail.com]

5. 实践:应用练习

练习 1. 获取长度大于 3 个字符的唯一用户名列表

List<String> longUniqueNames = names.stream()
    .filter(name -> name.length() > 3)
    .distinct()
    .collect(Collectors.toList());

System.out.println(longUniqueNames);
// 例如: [Alice, Denis]

练习 2. 从用户列表中获取第 3 和第 4 个唯一 email

List<String> thirdAndFourthEmail = users.stream()
    .map(user -> user.email)
    .distinct()
    .skip(2)
    .limit(2)
    .collect(Collectors.toList());

System.out.println(thirdAndFourthEmail);
// 例如: [eva@mail.com, gleb@mail.com]

练习 3. 获取大于 10 的前 5 个唯一数字

List<Integer> numbers = List.of(5, 12, 17, 5, 23, 17, 42, 19, 12, 8);

List<Integer> result = numbers.stream()
    .filter(n -> n > 10)
    .distinct()
    .limit(5)
    .collect(Collectors.toList());

System.out.println(result);
// 输出: [12, 17, 23, 42, 19]

6. 可视化示意:操作应用顺序

graph TD
    A[原始列表] --> B[filter]
    B --> C[distinct]
    C --> D[skip]
    D --> E[limit]
    E --> F[collect]

说明:
通常先使用 filter,然后用 distinct 去重,再使用 skiplimit,最后使用 collect。不过也可以根据具体需求调整顺序。

7. 使用 distinctlimitskip 的典型错误

错误 #1:以为 distinct 能按“任意”条件去重。
实际上,distinct 依据对象的 equals 方法工作。如果你想按某个字段(例如只按 email)来判定唯一性,而不是整个对象——要么重写 equals/hashCode,要么配合 Collectors.toMap() 或额外的过滤技巧来实现。

错误 #2:操作顺序不当。
如果先用 limit,再用 distinct,那么重复项可能仍然存在——因为你已经把流限制在前 N 个元素之内,而其中可能包含重复的值。

错误 #3:通过 skip 跳过的数量超过实际元素数。
如果尝试跳过的元素数多于流中实际的元素数,结果会是一个空列表——这不会抛错,但可能与你的预期不符。

错误 #4:忽视性能问题。
distinctlimitskip 在非常大的流上可能效率不高,尤其在无序流或元素本身较复杂的情况下。对 99% 的日常任务这不是问题,但如果你处理的是上百万条记录,就需要多加考虑。

错误 #5:忘记为自定义类重写 equals/hashCode
如果你在处理自定义对象(例如 User),没有重写这些方法,distinct 会把对象视为不同的,即使它们在“逻辑上”相同。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION