MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java Stream filter 方法的过滤策略

2023-12-036.4k 阅读

Java Stream filter 方法的过滤策略

1. Java Stream 简介

在深入探讨 filter 方法之前,先来了解一下 Java Stream。Java 8 引入了 Stream API,它为处理集合数据提供了一种更为高效、简洁且函数式的编程方式。Stream 代表了来自数据源的元素序列,支持顺序或并行的聚合操作。与传统的集合操作不同,Stream 操作并不会改变源数据,而是通过一系列的中间操作(如 filtermap 等)和终端操作(如 forEachcollect 等)产生新的结果。

例如,假设有一个整数列表 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 方法都能满足。但在使用过程中,需要注意性能考量和线程安全等问题,以确保程序的高效和正确性。