Java Stream filter 方法的过滤策略
Java Stream filter 方法的过滤策略
1. Java Stream 简介
在深入探讨 filter
方法之前,先来了解一下 Java Stream。Java 8 引入了 Stream API,它为处理集合数据提供了一种更为高效、简洁且函数式的编程方式。Stream 代表了来自数据源的元素序列,支持顺序或并行的聚合操作。与传统的集合操作不同,Stream 操作并不会改变源数据,而是通过一系列的中间操作(如 filter
、map
等)和终端操作(如 forEach
、collect
等)产生新的结果。
例如,假设有一个整数列表 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5)
,可以将其转换为 Stream 并对其进行操作:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.map(n -> n * 2)
.forEach(System.out::println);
上述代码将列表中的每个元素乘以 2 并打印出来。这里 stream()
方法将列表转换为 Stream,map
是中间操作,forEach
是终端操作。
2. filter 方法基础
filter
方法是 Stream API 中的一个中间操作,它的作用是根据给定的条件对 Stream 中的元素进行过滤,返回一个包含符合条件元素的新 Stream。其方法签名如下:
Stream<T> filter(Predicate<? super T> predicate);
其中 Predicate
是一个函数式接口,它接受一个参数并返回一个布尔值。
下面是一个简单的示例,从一个整数列表中过滤出偶数:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers);
在上述代码中,filter(n -> n % 2 == 0)
表示只保留能被 2 整除的元素,collect(Collectors.toList())
是终端操作,将过滤后的 Stream 转换回列表。
3. 复杂过滤条件
3.1 多条件与(AND)过滤
有时候需要满足多个条件才能保留元素,这可以通过 &&
操作符在 Predicate
中实现。例如,从一个学生列表中过滤出成绩大于 80 分且年龄小于 20 岁的学生:
class Student {
private String name;
private int age;
private double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
public int getAge() {
return age;
}
public double getScore() {
return score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
List<Student> students = Arrays.asList(
new Student("Alice", 18, 85),
new Student("Bob", 22, 78),
new Student("Charlie", 19, 90)
);
List<Student> filteredStudents = students.stream()
.filter(s -> s.getScore() > 80 && s.getAge() < 20)
.collect(Collectors.toList());
System.out.println(filteredStudents);
在 filter
方法的 Predicate
中,使用 &&
将两个条件连接起来,只有同时满足成绩大于 80 分且年龄小于 20 岁的学生才会被保留。
3.2 多条件或(OR)过滤
类似地,当需要满足多个条件中的任意一个时,可以使用 ||
操作符。例如,从一个员工列表中过滤出职位是 “Manager” 或者薪水大于 10000 的员工:
class Employee {
private String position;
private double salary;
public Employee(String position, double salary) {
this.position = position;
this.salary = salary;
}
public String getPosition() {
return position;
}
public double getSalary() {
return salary;
}
@Override
public String toString() {
return "Employee{" +
"position='" + position + '\'' +
", salary=" + salary +
'}';
}
}
List<Employee> employees = Arrays.asList(
new Employee("Engineer", 8000),
new Employee("Manager", 12000),
new Employee("Developer", 9000),
new Employee("Sales", 15000)
);
List<Employee> filteredEmployees = employees.stream()
.filter(e -> e.getPosition().equals("Manager") || e.getSalary() > 10000)
.collect(Collectors.toList());
System.out.println(filteredEmployees);
这里 filter
方法中的 Predicate
使用 ||
连接两个条件,只要满足职位是 “Manager” 或者薪水大于 10000 其中一个条件的员工就会被保留。
3.3 否定(NOT)过滤
有时候需要过滤掉满足某些条件的元素,即取反操作。可以通过 !
操作符来实现。例如,从一个字符串列表中过滤掉长度小于 5 的字符串:
List<String> words = Arrays.asList("apple", "banana", "cat", "dog", "elephant");
List<String> filteredWords = words.stream()
.filter(s ->!(s.length() < 5))
.collect(Collectors.toList());
System.out.println(filteredWords);
在 filter
方法的 Predicate
中,!(s.length() < 5)
表示过滤掉长度小于 5 的字符串,只保留长度大于等于 5 的字符串。
4. 结合其他 Stream 操作
filter
方法通常不会单独使用,而是与其他 Stream 操作结合,以实现更复杂的数据处理。
4.1 与 map 操作结合
先过滤再映射是一种常见的组合。例如,从一个整数列表中过滤出偶数并将其平方:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredEvenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(squaredEvenNumbers);
这里先使用 filter
方法过滤出偶数,然后使用 map
方法将这些偶数平方,最后通过 collect
方法转换为列表。
4.2 与 flatMap 操作结合
flatMap
可以将多个 Stream 合并为一个 Stream。结合 filter
可以实现更复杂的过滤和转换。例如,有一个包含多个字符串列表的列表,要过滤出长度大于 3 的字符串并合并到一个新的列表中:
List<List<String>> lists = Arrays.asList(
Arrays.asList("apple", "cat"),
Arrays.asList("banana", "dog", "elephant"),
Arrays.asList("fish", "goat")
);
List<String> filteredWords = lists.stream()
.flatMap(Collection::stream)
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
System.out.println(filteredWords);
在上述代码中,首先使用 flatMap
将多个字符串列表合并为一个 Stream,然后使用 filter
过滤出长度大于 3 的字符串,最后通过 collect
方法转换为列表。
4.3 与 reduce 操作结合
reduce
操作可以将 Stream 中的元素累积为一个值。结合 filter
可以对符合条件的元素进行累积计算。例如,从一个整数列表中过滤出奇数并计算它们的和:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sumOfOdds = numbers.stream()
.filter(n -> n % 2 != 0)
.reduce((a, b) -> a + b);
sumOfOdds.ifPresent(System.out::println);
这里先使用 filter
方法过滤出奇数,然后使用 reduce
方法将这些奇数累加起来。Optional
类型用于处理可能不存在值的情况(如 Stream 为空时)。
5. 并行 Stream 中的 filter
在处理大数据量时,并行 Stream 可以显著提高处理效率。filter
方法在并行 Stream 中同样适用,但需要注意一些性能和线程安全方面的问题。
例如,从一个包含大量整数的列表中过滤出偶数:
List<Integer> largeNumbers = IntStream.rangeClosed(1, 1000000)
.boxed()
.collect(Collectors.toList());
List<Integer> parallelEvenNumbers = largeNumbers.parallelStream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(parallelEvenNumbers.size());
在上述代码中,通过 parallelStream()
方法将列表转换为并行 Stream,然后使用 filter
方法过滤出偶数。并行 Stream 会将数据分成多个部分,在多个线程中并行处理,从而提高处理速度。
然而,在使用并行 Stream 时,如果 Predicate
中涉及到线程不安全的操作,可能会导致数据不一致或错误的结果。例如,假设 Predicate
中使用了一个非线程安全的计数器:
class Counter {
private int count = 0;
public boolean isEven(int num) {
count++;
return num % 2 == 0;
}
}
Counter counter = new Counter();
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> parallelFiltered = numbers.parallelStream()
.filter(counter::isEven)
.collect(Collectors.toList());
System.out.println("Counter value: " + counter.count);
在并行处理时,由于多个线程同时访问 counter
,会导致 count
的值不准确,因为 count++
操作不是线程安全的。为了避免这种情况,可以使用线程安全的计数器,如 AtomicInteger
:
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
class SafeCounter {
private AtomicInteger count = new AtomicInteger(0);
public boolean isEven(int num) {
count.incrementAndGet();
return num % 2 == 0;
}
}
SafeCounter safeCounter = new SafeCounter();
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> parallelFiltered = numbers.parallelStream()
.filter(safeCounter::isEven)
.collect(Collectors.toList());
System.out.println("Safe Counter value: " + safeCounter.count.get());
使用 AtomicInteger
可以确保在并行处理时 count
的值是准确的。
6. 自定义过滤策略
除了使用简单的 Predicate
表达式,还可以通过实现 Predicate
接口来自定义复杂的过滤策略。
例如,假设要从一个商品列表中过滤出符合特定促销条件的商品。促销条件是商品价格大于某个阈值且属于特定类别:
class Product {
private String category;
private double price;
public Product(String category, double price) {
this.category = category;
this.price = price;
}
public String getCategory() {
return category;
}
public double getPrice() {
return price;
}
@Override
public String toString() {
return "Product{" +
"category='" + category + '\'' +
", price=" + price +
'}';
}
}
class PromotionPredicate implements Predicate<Product> {
private double priceThreshold;
private String category;
public PromotionPredicate(double priceThreshold, String category) {
this.priceThreshold = priceThreshold;
this.category = category;
}
@Override
public boolean test(Product product) {
return product.getPrice() > priceThreshold && product.getCategory().equals(category);
}
}
List<Product> products = Arrays.asList(
new Product("Electronics", 500),
new Product("Clothing", 200),
new Product("Electronics", 800)
);
PromotionPredicate predicate = new PromotionPredicate(600, "Electronics");
List<Product> promotedProducts = products.stream()
.filter(predicate)
.collect(Collectors.toList());
System.out.println(promotedProducts);
在上述代码中,通过实现 Predicate
接口的 test
方法定义了自定义的过滤策略。PromotionPredicate
类接受价格阈值和商品类别作为参数,在 test
方法中判断商品是否符合促销条件。然后在 filter
方法中使用这个自定义的 Predicate
来过滤商品列表。
7. 性能考量
在使用 filter
方法时,性能是一个重要的考量因素。
7.1 大数据量时的性能
当处理大数据量时,合理使用 filter
方法对性能影响很大。例如,在一个包含数百万条记录的数据库查询结果转换为 Stream 进行过滤时,如果 Predicate
逻辑过于复杂,会导致性能下降。可以通过简化 Predicate
逻辑,或者使用并行 Stream 来提高性能。但需要注意并行 Stream 可能带来的线程安全问题,如前面所述。
7.2 多次过滤的性能
有时候可能会对 Stream 进行多次过滤。例如:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0)
.filter(n -> n > 5)
.collect(Collectors.toList());
虽然上述代码逻辑清晰,但从性能角度看,可以将两个 filter
合并为一个:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0 && n > 5)
.collect(Collectors.toList());
这样可以减少 Stream 中的中间操作,提高性能。因为每次中间操作都会产生一个新的 Stream,增加了计算开销。
8. 与传统循环过滤的对比
传统的过滤方式通常使用 for
循环或 while
循环。例如,从一个整数列表中过滤出偶数:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> evenNumbers = new ArrayList<>();
for (int num : numbers) {
if (num % 2 == 0) {
evenNumbers.add(num);
}
}
System.out.println(evenNumbers);
与使用 filter
方法相比,传统循环方式代码更加冗长,且不容易理解。filter
方法采用函数式编程风格,代码更简洁、易读,并且 Stream API 内部对性能进行了优化,在处理大数据量时往往表现更好。同时,Stream API 支持并行处理,而传统循环需要手动实现多线程处理,增加了代码的复杂性。
综上所述,Java Stream 的 filter
方法提供了强大而灵活的过滤策略,通过合理运用可以高效地处理各种数据过滤需求。无论是简单的条件过滤,还是复杂的多条件组合、自定义策略,以及在并行处理中的应用,filter
方法都能满足。但在使用过程中,需要注意性能考量和线程安全等问题,以确保程序的高效和正确性。