Перейти к основному содержимому

Математические и агрегатные операторы в RxJava

· 7 мин. чтения

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 , Average Long , Average Float и Average Double .

3.2. Максимум

Оператор 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 `` создает один элемент, содержащий наименьший элемент из источника:

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. Сумма

Оператор суммы выдает единственное значение, которое представляет собой сумму всех чисел, испускаемых исходным Observable:

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

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

subscriber.assertValue(210);

Существуют также примитивно-специализированные аналогичные операторы: sumInteger , sum Long , sum Float и sum Double .

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

4.1. Конкат

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

Давайте теперь определим два 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);

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

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

4.2. Считать

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

Давайте подсчитаем количество элементов, испускаемых 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);

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

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

4.3. Уменьшать

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

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

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

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

4.4. Собирать

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

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

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

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

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.5. К списку

Оператор toList работает так же, как операция сбора , но собирает все элементы в один список — подумайте о Collectors.toList() из 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));

4.6. 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));

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

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

4.7. Для отображения

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

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

  1. keySelector , который создает ключ из элемента
  2. селектор значений , который создает из созданного элемента фактическое значение, которое будет храниться на карте.
  3. mapFactory , которая создает коллекцию, в которой будут храниться элементы

Начнем с определения простого класса Book :

class Book {
private String title;
private Integer year;

// standard constructors, getters, and setters
}

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

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

4.8. ToMultiMap

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

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

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

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

5. Вывод

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

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