1. 方法 distinct:删除重复项
在实际开发中,我们不仅需要过滤和转换数据,还常常需要只从集合中选出唯一元素、限制结果大小,或者相反,跳过前面若干个元素。例如:
- 获取唯一的用户名列表。
- 只取前 10 条记录用于页面展示。
- 跳过前 5 个元素(例如实现分页——“从第 2 页开始显示”)。
针对这些需求,Stream API 提供了专门的方法:distinct、limit、skip。
distinct 如何工作?
方法 distinct() 会返回一个新流,其中所有重复元素都被删除。重复的判定由相应类的 equals 和 hashCode 方法决定。
打个比方:你是一名钱币收藏者,希望主藏品中没有重复。所有重复的硬币都进入交换库。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 对象默认没有重写 equals 和 hashCode。在这种情况下,distinct 只对“引用相同”的对象有效。
解决方案:重写 equals 和 hashCode,让唯一性由 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 按预期工作,在自定义类中一定要重写 equals 和 hashCode!
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. 组合使用 distinct、limit、skip
这些方法可以并且应该根据任务需要进行组合。
示例:从第 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 去重,再使用 skip 和 limit,最后使用 collect。不过也可以根据具体需求调整顺序。
7. 使用 distinct、limit、skip 的典型错误
错误 #1:以为 distinct 能按“任意”条件去重。
实际上,distinct 依据对象的 equals 方法工作。如果你想按某个字段(例如只按 email)来判定唯一性,而不是整个对象——要么重写 equals/hashCode,要么配合 Collectors.toMap() 或额外的过滤技巧来实现。
错误 #2:操作顺序不当。
如果先用 limit,再用 distinct,那么重复项可能仍然存在——因为你已经把流限制在前 N 个元素之内,而其中可能包含重复的值。
错误 #3:通过 skip 跳过的数量超过实际元素数。
如果尝试跳过的元素数多于流中实际的元素数,结果会是一个空列表——这不会抛错,但可能与你的预期不符。
错误 #4:忽视性能问题。
distinct、limit、skip 在非常大的流上可能效率不高,尤其在无序流或元素本身较复杂的情况下。对 99% 的日常任务这不是问题,但如果你处理的是上百万条记录,就需要多加考虑。
错误 #5:忘记为自定义类重写 equals/hashCode。
如果你在处理自定义对象(例如 User),没有重写这些方法,distinct 会把对象视为不同的,即使它们在“逻辑上”相同。
GO TO FULL VERSION