«1. Обзор

В этом уроке мы покажем, как фильтровать и преобразовывать коллекции с помощью Guava.

Мы будем фильтровать с помощью предикатов, преобразовывать с помощью функций, предоставляемых библиотекой, и, наконец, мы увидим, как сочетать фильтрацию и преобразование.

2. Фильтрация коллекции

Давайте начнем с простого примера фильтрации коллекции. Мы будем использовать готовый предикат, предоставленный библиотекой и созданный с помощью служебного класса Predicates:

@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"));
}

Как видите, мы фильтруем список имен, чтобы получить только те имена, которые содержат символ «a» — и для этого мы используем Iterables.filter().

В качестве альтернативы, мы также можем эффективно использовать API 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());
}

Здесь следует отметить несколько вещей: во-первых, вывод Collections.filter() представляет собой живое представление исходная коллекция — изменения в одной будут отражены в другой.

Также важно понимать, что теперь результат ограничен предикатом — если мы добавим элемент, который не удовлетворяет этому предикату, будет выброшено исключение 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. Напишите Custom Предикат фильтра

Далее — давайте напишем наш собственный предикат вместо того, чтобы использовать предикат, предоставленный библиотекой. В следующем примере мы определим предикат, который получает только имена, начинающиеся с «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. Объединение нескольких предикатов

Мы можем объединять несколько предикатов. используя 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. Удаляем нулевые значения при фильтрации коллекции ~ ~~ Мы можем удалить нулевые значения из коллекции, отфильтровав ее с помощью Predicates.notNull(), как в следующем примере:

6. Проверить, соответствуют ли все элементы в коллекции условию

@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"));
}

Далее, давайте проверим, все ли элементы в коллекции соответствуют определенному условию. Мы будем использовать Iterables.all(), чтобы проверить, все ли имена содержат «n» или «m», затем мы проверим, все ли элементы содержат «a»:

7. Преобразование коллекции

@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);
}

Теперь давайте посмотрим, как преобразовать коллекцию с помощью функции Guava. В следующем примере мы преобразуем список имен в список целых чисел (длина имени) с помощью Iterables.transform():

Мы также можем использовать API Collections2.transform(), как в следующий пример:

@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));
}

Обратите внимание, что выходные данные Collections.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());
}

И — как и раньше — если мы попытаемся добавить элемент в выходную коллекцию, будет выброшено исключение UnsupportedOperationException.

8. Создание функции из предиката

Мы также можем создать функцию из предиката, используя Functions.fromPredicate(). Это, конечно, будет функция, которая преобразует входные данные в логические значения в соответствии с условием предиката.

В следующем примере мы преобразуем список имен в список логических значений, где каждый элемент представляет, если имя содержит «m»:

9. Композиция двух функций

@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));
}

Далее — «Давайте посмотрим, как преобразовать коллекцию с помощью составной функции.

Functions.compose() возвращает композицию двух функций, поскольку она применяет вторую функцию к выходу первой функции.

В следующем примере первая Функция преобразует имя в его длину, затем вторая Функция преобразует длину в логическое значение, которое показывает, является ли длина имени четной:

10. Комбинированная фильтрация и преобразование

@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));
}

А теперь — давайте посмотрим на еще один классный API, который есть в Guava — тот, который действительно позволит нам объединять фильтрацию и преобразование в цепочку — FluentIterable.

В следующем примере мы фильтруем список имен, а затем преобразуем его с помощью FluentIterable:

Стоит отметить, что в некоторых случаях императивная версия более удобочитаема и ее следует предпочесть функциональный подход.

@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. Мы использовали API Collections2.filter() и Iterables.filter() для фильтрации, а также Collections2.transform() и Iterables.transform() для преобразования коллекций.

Наконец, мы кратко рассмотрели очень интересный FluentIterable Fluent API, сочетающий фильтрацию и преобразование.

Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub — это проект на основе Maven, поэтому его легко импортировать и запускать как есть.

«