«1. Введение

После введения в статью о RxJava мы рассмотрим агрегатные и математические операторы.

Эти операции должны ждать, пока исходный Observable выдаст все элементы. Из-за этого эти операторы опасно использовать с Observables, которые могут представлять очень длинные или бесконечные последовательности.

Во-вторых, во всех примерах используется экземпляр TestSubscriber, особая разновидность Subscriber, которую можно использовать для модульного тестирования, выполнения утверждений, проверки полученных событий или обертывания имитируемого Subscriber.

Теперь давайте рассмотрим математические операторы.

2. Настройка

Чтобы использовать дополнительные операторы, нам нужно добавить дополнительную зависимость в pom.xml:

<dependency>
    <groupId>io.reactivex</groupId>
    <artifactId>rxjava-math</artifactId>
    <version>1.0.0</version>
</dependency>

Или, для проекта Gradle:

compile 'io.reactivex:rxjava-math:1.0.0'

3. Математические операторы

MathObservable предназначен для выполнения математических операций, и его операторы используют другой Observable, который генерирует элементы, которые можно оценить как числа.

3.1. Среднее

Оператор среднего выдает одно значение — среднее значение всех значений, выдаваемых источником.

Давайте посмотрим на это в действии:

Observable<Integer> sourceObservable = Observable.range(1, 20);
TestSubscriber<Integer> subscriber = TestSubscriber.create();

MathObservable.averageInteger(sourceObservable).subscribe(subscriber);

subscriber.assertValue(10);

Есть четыре похожих оператора для работы с примитивными значениями: AverageInteger, AverageLong, AverageFloat и AverageDouble.

3.2. Max

Оператор max выдает наибольшее встречающееся число.

Давайте посмотрим на это в действии:

Observable<Integer> sourceObservable = Observable.range(1, 20);
TestSubscriber<Integer> subscriber = TestSubscriber.create();

MathObservable.max(sourceObservable).subscribe(subscriber);

subscriber.assertValue(9);

Важно отметить, что оператор max имеет перегруженный метод, который принимает функцию сравнения.

Учитывая тот факт, что математические операторы также могут работать с объектами, которыми можно управлять как числами, перегруженный оператор max позволяет сравнивать пользовательские типы или пользовательскую сортировку стандартных типов.

Давайте определим класс Item:

class Item {
    private Integer id;

    // standard constructors, getter, and setter
}

Теперь мы можем определить itemObservable, а затем использовать оператор max для создания Item с наивысшим идентификатором:

Item five = new Item(5);
List<Item> list = Arrays.asList(
  new Item(1), 
  new Item(2), 
  new Item(3), 
  new Item(4), 
  five);
Observable<Item> itemObservable = Observable.from(list);

TestSubscriber<Item> subscriber = TestSubscriber.create();

MathObservable.from(itemObservable)
  .max(Comparator.comparing(Item::getId))
  .subscribe(subscriber);

subscriber.assertValue(five);

3.3. Min

Оператор min создает один элемент, содержащий наименьший элемент из источника:

Observable<Integer> sourceObservable = Observable.range(1, 20);
TestSubscriber<Integer> subscriber = TestSubscriber.create();

MathObservable.min(sourceObservable).subscribe(subscriber);

subscriber.assertValue(1);

Оператор min имеет перегруженный метод, который принимает экземпляр компаратора:

Item one = new Item(1);
List<Item> list = Arrays.asList(
  one, 
  new Item(2), 
  new Item(3), 
  new Item(4), 
  new Item(5));
TestSubscriber<Item> subscriber = TestSubscriber.create();
Observable<Item> itemObservable = Observable.from(list);

MathObservable.from(itemObservable)
  .min(Comparator.comparing(Item::getId))
  .subscribe(subscriber);

subscriber.assertValue(one);

3.4. Sum

Оператор суммы выдает единственное значение, представляющее сумму всех чисел, выдаваемых источником.

Observable<Integer> sourceObservable = Observable.range(1, 20);
TestSubscriber<Integer> subscriber = TestSubscriber.create();

MathObservable.sumInteger(sourceObservable).subscribe(subscriber);

subscriber.assertValue(210);

4. Агрегатные операторы

4.1. Concat

Оператор concat объединяет элементы, испускаемые источником.

Давайте теперь определим два Observable и соединим их:

Вдаваясь в подробности, оператор concat ждет с подпиской на каждый дополнительный Observable, который передается ему, пока предыдущий не завершится.

List<Integer> listOne = Arrays.asList(1, 2, 3, 4);
Observable<Integer> observableOne = Observable.from(listOne);

List<Integer> listTwo = Arrays.asList(5, 6, 7, 8);
Observable<Integer> observableTwo = Observable.from(listTwo);

TestSubscriber<Integer> subscriber = TestSubscriber.create();

Observable<Integer> concatObservable = observableOne
  .concatWith(observableTwo);

concatObservable.subscribe(subscriber);

subscriber.assertValues(1, 2, 3, 4, 5, 6, 7, 8);

По этой причине объединение «горячего» Observable, который начинает испускать элементы немедленно, приведет к потере любых элементов, которые «горячий» Observable испускает до того, как все предыдущие будут завершены.

4.2. Count

Оператор count выводит количество всех элементов, испускаемых источником:

Давайте посчитаем количество элементов, испускаемых Observable:

Если исходный Observable завершается с ошибкой, счетчик передать ошибку уведомления без создания элемента. Однако, если он вообще не завершается, счетчик не выдает элемент и не завершается.

List<String> lettersList = Arrays.asList(
  "A", "B", "C", "D", "E", "F", "G");
TestSubscriber<Integer> subscriber = TestSubscriber.create();

Observable<Integer> sourceObservable = Observable
  .from(lettersList).count();
sourceObservable.subscribe(subscriber);

subscriber.assertValue(7);

Для операции подсчета также есть оператор countLong, который в конце выдает значение Long для тех последовательностей, которые могут превышать емкость целого числа.

4.3. Уменьшение

Оператор сокращения сводит все испускаемые элементы в один элемент, применяя функцию аккумулятора.

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

Теперь давайте посмотрим, как можно выполнить сокращение списка строк, объединяя их в обратном порядке:

4.4. Сбор

List<String> list = Arrays.asList("A", "B", "C", "D", "E", "F", "G");
TestSubscriber<String> subscriber = TestSubscriber.create();

Observable<String> reduceObservable = Observable.from(list)
  .reduce((letter1, letter2) -> letter2 + letter1);
reduceObservable.subscribe(subscriber);

subscriber.assertValue("GFEDCBA");

Оператор сбора похож на оператор сокращения, но предназначен для сбора элементов в единую изменяемую структуру данных.

Требуется два параметра:

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

    Давайте посмотрим, как можно вернуть набор элементов из Observable: ~~ ~

4.5. ToList

List<String> list = Arrays.asList("A", "B", "C", "B", "B", "A", "D");
TestSubscriber<HashSet> subscriber = TestSubscriber.create();

Observable<HashSet<String>> reduceListObservable = Observable
  .from(list)
  .collect(HashSet::new, HashSet::add);
reduceListObservable.subscribe(subscriber);

subscriber.assertValues(new HashSet(list));

4.6. ToSortedList

The toList operator works just like the collect operation, but collects all elements into a single list – think about Collectors.toList() from the Stream API:
Observable<Integer> sourceObservable = Observable.range(1, 5);
TestSubscriber<List> subscriber = TestSubscriber.create();

Observable<List<Integer>> listObservable = sourceObservable
  .toList();
listObservable.subscribe(subscriber);

subscriber.assertValue(Arrays.asList(1, 2, 3, 4, 5));

То же, что и в предыдущем примере, но сгенерированный список отсортирован:

Как мы видим, toSortedList использует сравнение по умолчанию, но можно предоставить пользовательскую функцию сравнения. Теперь мы можем увидеть, как можно сортировать целые числа в обратном порядке, используя пользовательскую функцию сортировки:

Observable<Integer> sourceObservable = Observable.range(10, 5);
TestSubscriber<List> subscriber = TestSubscriber.create();

Observable<List<Integer>> listObservable = sourceObservable
  .toSortedList();
listObservable.subscribe(subscriber);

subscriber.assertValue(Arrays.asList(10, 11, 12, 13, 14));

4.7. ToMap

Observable<Integer> sourceObservable = Observable.range(10, 5);
TestSubscriber<List> subscriber = TestSubscriber.create();

Observable<List<Integer>> listObservable 
  = sourceObservable.toSortedList((int1, int2) -> int2 - int1);
listObservable.subscribe(subscriber);

subscriber.assertValue(Arrays.asList(14, 13, 12, 11, 10));

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

В частности, оператор toMap имеет различные перегруженные методы, требующие одного, двух или трех следующих параметров:

Давайте начнем определение простого класса Book:

  1. the keySelector that produces a key from the item
  2. the valueSelector that produces from the emitted item the actual value that will be stored in the map
  3. the mapFactory that creates the collection that will hold the items

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

class Book {
    private String title;
    private Integer year;

    // standard constructors, getters, and setters
}

4.8. ToMultiMap

Observable<Book> bookObservable = Observable.just(
  new Book("The North Water", 2016), 
  new Book("Origin", 2017), 
  new Book("Sleeping Beauties", 2017)
);
TestSubscriber<Map> subscriber = TestSubscriber.create();

Observable<Map<String, Integer>> mapObservable = bookObservable
  .toMap(Book::getTitle, Book::getYear, HashMap::new);
mapObservable.subscribe(subscriber);

subscriber.assertValue(new HashMap() {{
  put("The North Water", 2016);
  put("Origin", 2017);
  put("Sleeping Beauties", 2017);
}});

При отображении очень часто многие значения используют один и тот же ключ. Структура данных, которая сопоставляет один ключ нескольким значениям, называется мультикартой.

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

Этот оператор добавляет еще один параметр к параметрам оператора toMap, collectionFactory. Этот параметр позволяет указать, в каком типе коллекции должно храниться значение. Давайте посмотрим, как это можно сделать:

5. Заключение

Observable<Book> bookObservable = Observable.just(
  new Book("The North Water", 2016), 
  new Book("Origin", 2017), 
  new Book("Sleeping Beauties", 2017)
);
TestSubscriber<Map> subscriber = TestSubscriber.create();

Observable multiMapObservable = bookObservable.toMultimap(
  Book::getYear, 
  Book::getTitle, 
  () -> new HashMap<>(), 
  (key) -> new ArrayList<>()
);
multiMapObservable.subscribe(subscriber);

subscriber.assertValue(new HashMap() {{
    put(2016, Arrays.asList("The North Water"));
    put(2017, Arrays.asList("Origin", "Sleeping Beauties"));
}});

В этой статье мы рассмотрели математические и агрегатные операторы, доступные в RxJava, и, конечно, простые примеры использования каждого из них.

Как всегда, все примеры кода в этой статье можно найти на Github.

«