1. 概述

本文将介绍如何使用 Guava 来对集合进行 过滤(filter)转换(transform) 操作。

我们将使用 Guava 提供的 Predicates 进行过滤,使用 Functions 进行转换,并展示如何将两者组合使用。

2. 集合过滤

先来看一个简单的集合过滤示例。我们将使用 Guava 提供的 Predicates 工具类创建的 Predicate 来进行过滤:

@Test
public void whenFilterWithIterables_thenFiltered() {
    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Iterable<String> result 
      = Iterables.filter(names, Predicates.containsPattern("a"));

    assertThat(result, containsInAnyOrder("Jane", "Adam"));
}

上面这段代码过滤了 names 列表,只保留包含字符 "a" 的名字。使用的是 Iterables.filter() 方法。

当然,也可以使用 Collections2.filter()

@Test
public void whenFilterWithCollections2_thenFiltered() {
    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Collection<String> result 
      = Collections2.filter(names, Predicates.containsPattern("a"));
    
    assertEquals(2, result.size());
    assertThat(result, containsInAnyOrder("Jane", "Adam"));

    result.add("anna");
    assertEquals(5, names.size());
}

⚠️ 注意:

  • Collections2.filter() 返回的是原始集合的 实时视图(live view),对结果集合的修改会影响原始集合。
  • 同时,由于结果集合被 Predicate 约束,如果添加不符合条件的元素,会抛出 IllegalArgumentException
@Test(expected = IllegalArgumentException.class)
public void givenFilteredCollection_whenAddingInvalidElement_thenException() {
    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Collection<String> result 
      = Collections2.filter(names, Predicates.containsPattern("a"));

    result.add("elvis");
}

3. 自定义过滤条件(Predicate)

除了使用 Guava 内置的 Predicate,我们也可以自己实现。例如下面这个 Predicate 会筛选出以 "A""J" 开头的名字:

@Test
public void whenFilterCollectionWithCustomPredicate_thenFiltered() {
    Predicate<String> predicate = new Predicate<String>() {
        @Override
        public boolean apply(String input) {
            return input.startsWith("A") || input.startsWith("J");
        }
    };

    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Collection<String> result = Collections2.filter(names, predicate);

    assertEquals(3, result.size());
    assertThat(result, containsInAnyOrder("John", "Jane", "Adam"));
}

4. 组合多个 Predicate

可以通过 Predicates.or()Predicates.and() 来组合多个条件。

下面的例子筛选出以 "J" 开头 不包含 "a" 的名字:

@Test
public void whenFilterUsingMultiplePredicates_thenFiltered() {
    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Collection<String> result = Collections2.filter(names, 
      Predicates.or(Predicates.containsPattern("J"), 
      Predicates.not(Predicates.containsPattern("a"))));

    assertEquals(3, result.size());
    assertThat(result, containsInAnyOrder("John", "Jane", "Tom"));
}

5. 过滤掉集合中的 null 值

使用 Predicates.notNull() 可以轻松地过滤掉 null 值:

@Test
public void whenRemoveNullFromCollection_thenRemoved() {
    List<String> names = 
      Lists.newArrayList("John", null, "Jane", null, "Adam", "Tom");
    Collection<String> result = 
      Collections2.filter(names, Predicates.notNull());

    assertEquals(4, result.size());
    assertThat(result, containsInAnyOrder("John", "Jane", "Adam", "Tom"));
}

6. 检查集合中所有元素是否满足条件

使用 Iterables.all() 可以判断集合中是否所有元素都满足某个条件。

下面的例子检查是否所有名字都包含 "n""m",然后再检查是否都包含 "a"

@Test
public void whenCheckingIfAllElementsMatchACondition_thenCorrect() {
    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");

    boolean result = Iterables.all(names, Predicates.containsPattern("n|m"));
    assertTrue(result);

    result = Iterables.all(names, Predicates.containsPattern("a"));
    assertFalse(result);
}

7. 集合转换

使用 Guava 的 Function 可以对集合进行转换操作。下面的例子将名字列表转换为名字长度的列表:

@Test
public void whenTransformWithIterables_thenTransformed() {
    Function<String, Integer> function = new Function<String, Integer>() {
        @Override
        public Integer apply(String input) {
            return input.length();
        }
    };

    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Iterable<Integer> result = Iterables.transform(names, function);

    assertThat(result, contains(4, 4, 4, 3));
}

也可以使用 Collections2.transform()

@Test
public void whenTransformWithCollections2_thenTransformed() {
    Function<String,Integer> func = new Function<String,Integer>(){
        @Override
        public Integer apply(String input) {
            return input.length();
        }
    };

    List<String> names = 
      Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Collection<Integer> result = Collections2.transform(names, func);

    assertEquals(4, result.size());
    assertThat(result, contains(4, 4, 4, 3));

    result.remove(3);
    assertEquals(3, names.size());
}

⚠️ 注意:

  • Collections2.transform() 返回的也是原始集合的 实时视图
  • 如果尝试向转换后的集合中添加元素,会抛出 UnsupportedOperationException

8. 由 Predicate 创建 Function

可以通过 Functions.forPredicate()Predicate 转换为 Function,该函数会将输入转换为布尔值:

@Test
public void whenCreatingAFunctionFromAPredicate_thenCorrect() {
    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Collection<Boolean> result =
      Collections2.transform(names,
      Functions.forPredicate(Predicates.containsPattern("m")));

    assertEquals(4, result.size());
    assertThat(result, contains(false, false, true, true));
}

9. 函数组合

使用 Functions.compose() 可以将两个 Function 组合起来:第二个函数作用于第一个函数的输出。

下面的例子中,第一个函数获取名字长度,第二个判断长度是否为偶数:

@Test
public void whenTransformingUsingComposedFunction_thenTransformed() {
    Function<String,Integer> f1 = new Function<String,Integer>(){
        @Override
        public Integer apply(String input) {
            return input.length();
        }
    };

    Function<Integer,Boolean> f2 = new Function<Integer,Boolean>(){
        @Override
        public Boolean apply(Integer input) {
            return input % 2 == 0;
        }
    };

    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Collection<Boolean> result = 
      Collections2.transform(names, Functions.compose(f2, f1));

    assertEquals(4, result.size());
    assertThat(result, contains(true, true, true, false));
}

10. 过滤与转换的链式操作

Guava 提供了一个非常方便的链式 API:FluentIterable,可以将过滤和转换操作串联起来。

下面的例子先过滤出以 "A""T" 开头的名字,再转换为名字长度:

@Test
public void whenFilteringAndTransformingCollection_thenCorrect() {
    Predicate<String> predicate = new Predicate<String>() {
        @Override
        public boolean apply(String input) {
            return input.startsWith("A") || input.startsWith("T");
        }
    };

    Function<String, Integer> func = new Function<String,Integer>(){
        @Override
        public Integer apply(String input) {
            return input.length();
        }
    };

    List<String> names = Lists.newArrayList("John", "Jane", "Adam", "Tom");
    Collection<Integer> result = FluentIterable.from(names)
                                               .filter(predicate)
                                               .transform(func)
                                               .toList();

    assertEquals(2, result.size());
    assertThat(result, containsInAnyOrder(4, 3));
}

✅ 虽然函数式风格很优雅,但在某些场景下,命令式代码可能更清晰,建议根据实际可读性权衡选择。

11. 总结

本文介绍了如何使用 Guava 对集合进行过滤和转换:

  • 过滤:Collections2.filter()Iterables.filter()
  • 转换:Collections2.transform()Iterables.transform()
  • 组合操作:FluentIterable 提供了链式调用的 API

所有示例代码均可在 GitHub 项目 中找到,这是一个基于 Maven 的项目,可直接导入运行。


原始标题:Filtering and Transforming Collections in Guava