Java函数式编程之【过滤器filter合并】【predicate(断言)】与【谓词逻辑】
- 一、合并多个过滤器filter (Lambda版本)
- 二、合并多个过滤器filter (谓词逻辑(Predicate)版本)
- (一)使用基础谓词合并
- (二)使用高级谓词合并
- (三)使用带参数的可配置谓词合并
- (四)使用复杂的谓词工厂合并
一、合并多个过滤器filter (Lambda版本)
合并多个filter()操作是一个非常重要且常见的优化策略。它的核心思想是:通过逻辑运算符将多个过滤条件合并为一个filter(),减少Stream流管道中的阶段数量和 predicate(断言)的执行次数。
经典示例:筛选用户列表
我们先定义一个User用户类:
package predicate;public class User {public enum Status { ACTIVE, INACTIVE, PENDING }private String name;private int age;private Status status;private int loginCount;public User(String name,int age,Status status,int n) {this.name = name;this.age = age;this.status = status;loginCount = n;}public String getName() {return name;}public int getAge() { return age; }public Status getStatus() { return status; }public int getLoginCount() { return loginCount; }
}
假设我们有一个User对象列表,我们需要筛选出同时满足以下条件的用户:
- 年龄大于等于18岁(成年人)
- 年龄小于65岁(未退休)
- 账户状态是激活的(ACTIVE)
- 登录次数大于0(活跃用户)
低效做法:使用多个连续的 filter 操作
public class PredicateDemo {private static List<User> users = new ArrayList<>(Arrays.asList(new User("张三", 32, Status.ACTIVE, 2),new User("李四", 16, Status.INACTIVE, 0),new User("王明", 72, Status.ACTIVE, 1),new User("王五", 35, Status.ACTIVE, 5),new User("钱多多", 68, Status.PENDING, 2)));public static void testA() {List<User> filteredUsers = users.stream().filter(user -> user.getAge() >= 18) // 第一轮过滤:检查条件1.filter(user -> user.getAge() < 65) // 第二轮过滤:检查条件2.filter(user -> user.getStatus() == User.Status.ACTIVE) // 第三轮过滤:检查条件3.filter(user -> user.getLoginCount() > 0) // 第四轮过滤:检查条件4.collect(Collectors.toList());}}
在这个做法中,数据仿佛要“过四道筛子”:
- 第一个filter处理整个列表,生成一个包含所有成年人的新流。
- 第二个filter处理上一个流,生成一个包含18-65岁用户的新流。
- 第三个和第四个filter继续重复这个过程。
一个最终被淘汰的用户(例如,一个17岁的用户)可能只在第一轮过滤时就被排除了,但它仍然需要经历后续所有filter阶段的 predicate 检查吗?不,实际上不会,但它的处理流程仍然有开销。
高效做法:合并 filter 操作
public static void testB() {List<User> filteredUsers = users.stream().filter(user -> user.getAge() >= 18 && user.getAge() < 65&& user.getStatus() == User.Status.ACTIVE&& user.getLoginCount() > 0).collect(Collectors.toList());}
其实,在本例中还可以继续进行优化。
利用逻辑运算的短路(Short-Circuit)评估: 这也是一个关键的优化点。Java中的逻辑运算符 && (AND) 和 || (OR) 是短路的。
1)、对于 逻辑运算符 && (AND):如果第一个条件为 false,整个表达式的结果立即确定为 false,后续条件将不再被计算。
在本例中,如果一个用户年龄是17岁(user.getAge() >= 18 为 false),JVM会立即跳过对其退休状态、账户状态和登录次数的检查。这节省了3次方法调用和比较操作。
将最可能失败或计算成本最低的条件放在&&的最左边,可以最大化短路优化带来的收益。
合并后,条件的顺序变得很重要,因为它直接影响短路评估的效果。
进一步优化策略:
-
- 将最可能使条件失败的条件放在前面。 如果90%的用户都是非活跃的,那么先检查loginCount > 0就能最快地淘汰大部分元素。
-
- 将计算成本最低的条件放在前面。 比较年龄(基本类型比较)的成本远低于从数据库或网络获取状态(假设getStatus()很昂贵)。先进行廉价的检查,可以避免对即将被淘汰的元素执行昂贵的操作。
优化合并filter()操作的 最终版本:
//优化合并filter()操作的最终版本
List<User> users = // ... 获取用户列表List<User> filteredUsers = users.stream().filter(user -> user.getLoginCount() > 0 // 成本低,可能淘汰很多用户&& user.getAge() >= 18 // 成本低&& user.getAge() < 65 // 成本低&& user.getStatus() == User.Status.ACTIVE) // 可能成本最高(例如涉及数据库查询),放在最后
注意事项: 如果某个昂贵条件是独立的,无法通过短路避免,有时将其单独放在一个filter里并在前面使用更廉价的条件过滤可能更优,但这需要具体分析。通常,合并filter并利用短路特性来调整过滤条件顺序是最佳实践。
2)、对于 逻辑运算符 || (OR):如果第一个条件为 true,整个表达式立即为 true,后续条件不再计算。
请看一个 逻辑运算符 **|| (OR)**逻辑连接的filter() 操作合并为一个操作的示例:
// 多个filter() 合并 和 优化 示例List<User> users = // ... 获取用户列表
// 未合并为一个filter (OR 逻辑)的原始状态
List<User> filteredUsers = users.stream().filter(user -> user.getTotalSpending() > 1000).filter(user -> user.getYearsAsMember() > 5)...// 简单合并为一个filter (OR 逻辑)
List<User> filteredUsers = users.stream().filter(user -> user.getTotalSpending() > 1000 || user.getYearsAsMember() > 5)...// 优化后: 优化策略:将更可能为true或成本更低的条件放在||前面
List<User> filteredUsers = users.stream().filter(user -> user.getYearsAsMember() > 5 || user.getTotalSpending() > 1000)...
结论: 可将多个连续的、用 && (AND) 或 || (OR) 逻辑连接的filter() 操作合并为一个,并利用短路评估来排序条件,是减少不必要的计算、提升Stream性能的经典且高效的手段。在编写Stream代码时,应有意识地检查是否存在可合并的filter()操作。
二、合并多个过滤器filter (谓词逻辑(Predicate)版本)
使用谓词逻辑(Predicate)来重写过滤器filter()合并操作是一个非常优雅且强大的方式。它既保持了性能优势,又提升了代码的可读性、复用性和声明性。
(一)使用基础谓词合并
首先,我们定义各个独立的过滤条件作为 Predicate 对象。
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;// 定义独立的谓词
Predicate<User> isAdult = user -> user.getAge() >= 18;
Predicate<User> isNotRetired = user -> user.getAge() < 65;
Predicate<User> isActive = user -> user.getStatus() == User.Status.ACTIVE;
Predicate<User> hasLoggedIn = user -> user.getLoginCount() > 0;
现在,我们可以用多种方式组合这些谓词:
方式一:使用 and() 方法进行逻辑与组合
这是最直接和推荐的方式,非常清晰地表达了“所有条件都必须满足”的逻辑。
// 独立的谓词用法:使用 and() 方法进行逻辑与组合public static void test01() { /*** 独立的谓词用法 ***/// 定义独立的谓词Predicate<User> isAdult = user -> user.getAge() >= 18;Predicate<User> isNotRetired = user -> user.getAge() < 65;Predicate<User> isActive = user -> user.getStatus() == User.Status.ACTIVE;Predicate<User> hasLoggedIn = user -> user.getLoginCount() > 0;List<User> filteredUsers = users.stream().filter(isAdult.and(isNotRetired).and(isActive).and(hasLoggedIn)) // 将多个谓词用 AND 逻辑连接.collect(Collectors.toList());filteredUsers.forEach(user->System.out.println(user.getName()));}
方式二:使用静态组合方法
可以通过一个静态工具方法将组合逻辑封装起来,使流操作更加简洁。
/*** 在一个工具类中定义组合谓词:使用静态组合方法可以通过一个静态工具方法将组合逻辑封装起来,使流操作更加简洁。***/public static void test02() { /*** 使用静态组合方法 ***///在Stream中使用List<User> filteredUsers = users.stream().filter(UserPredicates.isEligibleActiveUser()).collect(Collectors.toList());filteredUsers.forEach(user->System.out.println(user.getName()));}
下面这个是示例中用到的静态工具方法:
package predicate;import java.util.function.Predicate;//在一个工具类中定义组合谓词
public class UserPredicates {// 定义谓词static Predicate<User> isAdult = user -> user.getAge() >= 18;static Predicate<User> isNotRetired = user -> user.getAge() < 65;static Predicate<User> isActive = user -> user.getStatus() == User.Status.ACTIVE;static Predicate<User> hasLoggedIn = user -> user.getLoginCount() > 0;public static Predicate<User> isEligibleActiveUser() {return isAdult.and(isNotRetired).and(isActive).and(hasLoggedIn);}
}
(二)使用高级谓词合并
高级谓词操作
谓词逻辑的强大之处在于它可以轻松处理更复杂的组合逻辑。
示例:定义“高价值用户”谓词
Predicate<User> isHighValue = user -> user.getTotalPurchases() > 1000;
Predicate<User> isLongTermMember = user -> user.getMembershipYears() >= 3;// 组合逻辑:要么是高价值用户,要么是长期会员的活跃成人用户
Predicate<User> isSpecialUser = isAdult.and(isActive).and(isHighValue.or(isLongTermMember)); // 嵌套 OR 逻辑List<User> specialUsers = users.stream().filter(isSpecialUser).collect(Collectors.toList());
示例:使用 negate() 进行逻辑非操作
// 示例:使用 negate() 进行逻辑非操作public static void test04() {Predicate<User> isAdult = user -> user.getAge() >= 18;Predicate<User> isActive = user -> user.getStatus() == User.Status.ACTIVE;Predicate<User> isInactive = isActive.negate(); // 非活跃用户Predicate<User> isMinor = isAdult.negate(); // 未成年人// 获取非活跃的或未成年的用户List<User> excludedUsers = users.stream().filter(isInactive.or(isMinor)).collect(Collectors.toList());}
(三)使用带参数的可配置谓词合并
带参数的可配置谓词:如果需要动态条件,可以创建返回 Predicate 的方法。
// 创建动态谓词生成方法/*** 创建返回 Predicate 的方法。 ***/public static Predicate<User> ageInRange(int min, int max) {return user -> user.getAge() >= min && user.getAge() <= max;}public static Predicate<User> minLoginCount(int threshold) {return user -> user.getLoginCount() >= threshold;}public static void test05() {Predicate<User> isActive = user -> user.getStatus() == User.Status.ACTIVE;// 使用动态谓词int minLogin = 5;int maxAge = 60;List<User> filteredUsers = users.stream().filter(ageInRange(25, maxAge).and(minLoginCount(minLogin)).and(isActive)).collect(Collectors.toList());filteredUsers.forEach(user->System.out.println(user.getName()));}
(四)使用复杂的谓词工厂合并
复杂的复合谓词工厂:对于非常复杂的业务逻辑,可以创建一个专门的谓词工厂类。
package predicate;import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import predicate.User.Status;/*** 对于非常复杂的业务逻辑,可以创建一个专门的谓词工厂类。***/
public class UserPredicateFactory {private static List<User> users = new ArrayList<>(Arrays.asList(new User("张三", 32, Status.ACTIVE, 2),new User("李四", 16, Status.INACTIVE, 0),new User("王明", 72, Status.ACTIVE, 1),new User("王五", 35, Status.ACTIVE, 5),new User("钱多多", 68, Status.PENDING, 2)));public static Predicate<User> createEligibilityPredicate(boolean requireActive, int minAge, int minLogins,User.Status... allowedStatuses) {Predicate<User> predicate = user -> user.getAge() >= minAge;if (requireActive) {predicate = predicate.and(user -> user.getLoginCount() >= minLogins);}if (allowedStatuses != null && allowedStatuses.length > 0) {// allowedStatuses 是一个 User.Status 数组Set<User.Status> statusSet = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(allowedStatuses)));predicate = predicate.and(user -> statusSet.contains(user.getStatus()));}return predicate;}public static void main(String[] args) {// 使用工厂创建谓词Predicate<User> customEligibility = UserPredicateFactory.createEligibilityPredicate(true, 18, 1, User.Status.ACTIVE, User.Status.PENDING);List<User> result = users.stream().filter(customEligibility).collect(Collectors.toList());result.forEach(user->System.out.println(user.getName()));}
}
总结:
为什么谓词逻辑方式更优秀?
- 可复用性:单个谓词(如 isAdult)可以在代码库的多个地方重用。
- 可读性:.and(), .or(), .negate() 等方法让组合逻辑变得非常清晰,就像书写数学逻辑表达式一样。
- 可测试性:每个谓词都可以单独进行单元测试,组合逻辑也可以独立测试。
- 可维护性:当业务规则变化时,只需要修改对应的谓词定义,而不用到处修改Stream管道。
- 声明式编程:代码更专注于"做什么"而不是"怎么做",符合函数式编程 philosophy。
- 保持性能优势:底层仍然使用短路评估,性能与直接使用 && 运算符相当。
谓词逻辑方式的唯一缺点是会创建更多的对象(每个Predicate实例),但在绝大多数应用场景中,这种开销相对于它带来的好处和Stream操作本身的开销来说是微不足道的。