2.3 Predicate接口
问题
用户希望使用 java.util.function.Predicate 接口筛选数据。
方案
使用 lambda 表达式或方法引用来实现 boolean test(T t) 方法。
讨论
Predicate 接口主要用于流的筛选。给定一个包含若干项的流,Stream 接口的 filter 方法传入 Predicate 并返回一个新的流,它仅包含满足给定谓词的项。
Predicate 接口包含的单一抽象方法为 boolean test(T t),它传入一个泛型参数并返回 true 或 false。例 2-6 列出了 Predicate 接口定义的所有方法(包括静态和默认方法)。
例 2-6
Predicate接口定义的方法
- default Predicate<T> and(Predicate<? super T> other)
- static <T> Predicate<T> isEqual(Object targetRef)
- default Predicate<T> negate()
- default Predicate<T> or(Predicate<? super T> other)
- boolean test(T t) ➊
➊ 单一抽象方法
给定一个名称集合,可以通过流处理找出所有具有特定长度的实例,如例 2-7 所示。
例 2-7 查找具有给定长度的字符串
- public String getNamesOfLength(int length, String... names) {
- return Arrays.stream(names)
- .filter(s -> s.length() == length) ➊
- .collect(Collectors.joining(", "));
- }
➊ 满足给定长度字符串的谓词
或者,我们也可能只需要返回以特定字符串开头的名称,如例 2-8 所示。
例 2-8 查找以给定字符串开头的字符串
- public String getNamesStartingWith(String s, String... names) {
- return Arrays.stream(names)
- .filter(str -> str.startsWith(s)) ➊
- .collect(Collectors.joining(", "));
- }
➊ 返回以给定字符串开头的字符串
如果允许客户端指定条件,Predicate 的通用性会更强。例 2-9 显示了其中一种应用。
例 2-9 查找满足任意谓词的字符串
- public class ImplementPredicate {
- public String getNamesSatisfyingCondition(
- Predicate<String> condition, String... names) {
- return Arrays.stream(names)
- .filter(condition) ➊
- .collect(Collectors.joining(", "));
- }
- }
- // 其他方法
- }
➊ 根据提供的谓词进行筛选
上述用法相当灵活,但依靠客户端自己编写所有谓词或许不太容易。一种方案是将常量添加到代表最常见情况的类中,如例 2-10 所示。
例 2-10 为常见情况添加常量
- public class ImplementPredicate {
- public static final Predicate<String> LENGTH_FIVE = s -> s.length() == 5;
- public static final Predicate<String> STARTS_WITH_S =
- s -> s.startsWith("S");
- // 其余代码和之前一样
- }
提供谓词作为参数的另一个优点是,可以使用默认方法 and、or 与 negate,并根据一系列单个元素来创建复合谓词(composite predicate)。
例 2-11 的测试用例展示了各种方法的应用。
- import static functionpackage.ImplementPredicate.*; ➊
- import static org.junit.Assert.assertEquals;
- // 其他导入
- public class ImplementPredicateTest {
- private ImplementPredicate demo = new ImplementPredicate();
- private String[] names;
- @Before
- public void setUp() {
- names = Stream.of("Mal", "Wash", "Kaylee", "Inara", "Zoë",
- "Jayne", "Simon", "River", "Shepherd Book")
- .sorted()
- .toArray(String[]::new);
- }
- @Test
- public void getNamesOfLength5() throws Exception {
- assertEquals("Inara, Jayne, River, Simon",
- demo.getNamesOfLength(5, names));
- }
- @Test
- public void getNamesStartingWithS() throws Exception {
- assertEquals("Shepherd Book, Simon",
- demo.getNamesStartingWith("S", names));
- }
- @Test
- public void getNamesSatisfyingCondition() throws Exception {
- assertEquals("Inara, Jayne, River, Simon",
- demo.getNamesSatisfyingCondition(s -> s.length() == 5, names));
- assertEquals("Shepherd Book, Simon",
- demo.getNamesSatisfyingCondition(s -> s.startsWith("S"),
- names));
- assertEquals("Inara, Jayne, River, Simon",
- demo.getNamesSatisfyingCondition(LENGTH_FIVE, names));
- assertEquals("Shepherd Book, Simon",
- demo.getNamesSatisfyingCondition(STARTS_WITH_S, names));
- }
- @Test
- public void composedPredicate() throws Exception {
- assertEquals("Simon",
- demo.getNamesSatisfyingCondition(
- LENGTH_FIVE.and(STARTS_WITH_S), names)); ➋
- assertEquals("Inara, Jayne, River, Shepherd Book, Simon",
- demo.getNamesSatisfyingCondition(
- LENGTH_FIVE.or(STARTS_WITH_S), names)); ➋
- assertEquals("Kaylee, Mal, Shepherd Book, Wash, Zoë",
- demo.getNamesSatisfyingCondition(LENGTH_FIVE.negate(), names)); ➌
- }
- }
❶ 静态导入让使用常量更加简单
❷ 复合
❸ 否定
标准库还支持 Predicate 接口的其他一些用法。
Optional.filter(Predicate predicate)
如果值存在且匹配某个给定的谓词,则返回描述该值的 Optional,否则返回一个空 Optional。
Collection.removeIf(Predicate filter)
删除集合中所有满足谓词的元素。
Stream.allMatch(Predicate predicate)
如果流的所有元素均满足给定的谓词,则返回 true。anyMatch 和 noneMatch 方法的用法与之类似。
Collectors.partitioningBy(Predicate predicate)
返回一个 Collector,它将流分为两类(满足谓词和不满足谓词)。
只要流仅返回特定的元素,Predicate 接口就很有用。希望本范例能对读者有所启发,理解这种接口的用法。
另见
有关闭包复合(closure composition)的讨论请参见范例 5.8,有关 allMatch、anyMatch 与 noneMatch 方法的讨论请参见范例 3.10,有关分区和分组的讨论请参见范例 4.5。
