RxJava набирает все большую популярность в нынешнее время. На ней написаны многие библиотеки для Java и Android, а обладание знаниями в этой области стало синонимом избранности. В резюме строка с описанием того что вы спец в реактивном программировании поднимает вашу привлекательность для работодателей перед вашими конкурентами.

И вот я тоже решил присоединиться к этому течению, освоить и начать применять в своих проектах RxJava. В процессе чтения нескольких книг и статей все было абсолютно понятно. «Эти знания и правда в разы укоротят код и придадут читабельности» — думал я. Но как только книги были закончены и я сел переписывать свой проект на реактивщину — стало понятно что я не понимаю даже с чего начинать…

Еще куча времени было потрачено на практику и блуждание в операторах. Для большей эффективности я придумал себе несколько задачек, и поставил целью решить их, применяя только операторы Rx. Предлагаю вам тоже испытать свои знания!

Если вы считаете себя специалистом в этой области — открывайте IDE и попробуйте решить пять задачек. Предполагается что вы обладаете начальными знаниями и с легкостью напишете простой Observable. Только давайте условимся — не подсматривайте в решение сразу! Ведь уверенные знания рождаются только в усердной практике. В данных задачах не буду проводить описание операторов, которые были использованы. Если какие то задачи вызывают затруднения — просто остановитесь — загляните сюда и попробуйте выбрать тот оператор, который поможет вам решить нужную задачу.

Давайте начнем с очень простой задачи:


Задача 1:

У вас есть список городов, уложенный в контейнер List. Необходимо вывести его на печать.

Исходные данные:

List<String> cities = Arrays.asList("Париж", "Лондон", "Рим", "Москва");

Что в результате должны увидеть в консоли:

город Париж
город Лондон
город Рим
город Москва

Решение:
Observable.from(list)
       .map(s -> "город "+s)
       .subscribe(System.out::println);


Задача проще простого! Уверен вы решили ее мгновенно. Давайте попробуем что нибудь посложнее.

Задача 2:

У вас есть список городов и стран, уложенных в контейнеры List. Необходимо вывести список, упорядоченный по алфавиту, в формате <название города> — столица <название страны> на печать.

Исходные данные:

List<String> cities = Arrays.asList("Париж", "Лондон", "Рим", "Москва");
List<String> countries = Arrays.asList("Франция", "Англия", "Италия", "Россия");

Что в результате ожидаем увидеть в консоли:

Лондон — столица Англии
Москва — столица России
Париж — столица Франции
Рим — столица Италии

Решение:
Observable.from(countries)
       .map(s -> s.substring(0, s.length() - 1).concat("и"))
       .zipWith(cities, (country, city) -> city+" - столица "+country)
       .toSortedList()
       .flatMap(Observable::from)
       .subscribe(System.out::println);

Задача 3:

Вывести на печать все простые числа меньше ста.

Для справки:

Вдруг кто забыл — простые числа те, что делятся без остатка только на себя и на единицу. 0 и 1 за простые числа принято не считать

Исходные данные:

Для облегчения задачи — метод для определения простого числа:

private boolean isPrime(int n) {
   for(int i=2; i<n; i++) {
       if (n % i == 0) return false;
   }
   return true;
}

Что в результате должны увидеть в консоли:

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

Решение
Observable.range(0, Integer.MAX_VALUE)
       .skip(2)
       .filter(this::isPrime)
       .takeWhile(integer -> integer < 100)
       .map(String::valueOf)
       .reduce((s1, s2) -> s1+" "+s2)
       .subscribe(System.out::println);


Задача 4:

Определить какие числа, в промежутку от 1 до 20 делятся без остатка на 3 и 7 соответственно.

Исходные данные:

Для справки — для разделения на разные типы делителей воспользовался вот таким методом:

private int whatDivider(int n) {
        if (n % 3 == 0) return 3;
        if (n % 7 == 0) return 7;
        return 0;
    }

Что в результате должны увидеть в консоли:

Делится без остатка на 3: 3 6 9 12 15 18
Делится без остатка на 7: 7 14

Решение:
Observable.range(1, 20)
                .groupBy(this::whatDivider)
                .filter(observable -> observable.getKey() != 0)
                .subscribe(observable -> observable
                        .map(String::valueOf)
                        .reduce((s1, s2) -> s1+" "+s2)
                        .map(s -> "Делится без остатка на "+observable.getKey()+": "+s)
                        .subscribe(System.out::println));


Задача 5:

Необходимо найти сумму первых десяти членов ряда Фибоначчи.

Для справки:

Ряд Фибоначчи — каждое последующее число равно сумме двух предыдущих чисел, и первые десять членов ряда его — 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, и их сумма равна 88.

Исходные данные:

Для решения задачи я воспользовался написанным рядом классом Pair:

private static class Pair {
   final int n1;
   final int n2;

   	  Pair(int n1, int n2) {
 	      this.n1 = n1;
 	      this.n2 = n2;
   }
}

Если получится обойтись без него — будет рад поучиться у вас!

Что в результате должны увидеть в консоли:

88

Решение:
Observable.range(0, 9)
       .map(integer -> new Pair(0, 1))
       .scan((source, result) -> new Pair(source.n2, source.n1 + source.n2))
       .map(pair -> pair.n2)
       .reduce((integer, integer2) -> integer + integer2)
       .subscribe(System.out::println);


Если ваши решения получились лаконичнее, чем приведены здесь — пишите в комментариях, буду рад подчерпнуть новые способы, так как я не претендую на абсолютные знания в области Rx и сам только учусь. Пишите если вам понравилось, на примете есть еще цикл задачек с использованием кастомных операторов, и если вам будет интересно — попробую продолжить идею в следующих статьях.
Поделиться с друзьями
-->

Комментарии (24)


  1. t13s
    16.03.2017 14:58

    А можете посоветовать какую-нибудь литературу по Rx, которая мозги в нужную сторону вправляет?
    Ибо документация — это, конечно, хорошо, «понятно», и всё такое…
    Но в моих чертогах разума Rx до сих пор лежит где-то за дверью «Не влезай — убьёт» на одной полочке с Brainfuck-ом.


    1. Netyaga
      16.03.2017 15:25

      Я прочел книгу Reactive Programming with RxJava, насколько я помню она есть и на русском. Покороче можно прочесть эту — Nickolay Tsvetinov: Learning Reactive Programming With Java 8


      1. xGromMx
        16.03.2017 15:37

        использовать subscribe в subscribe это не правильно, для этого есть сглаживающие операторы типа {concat, flat}Map


        1. Netyaga
          16.03.2017 16:13

          Мне тоже не очень понравилось такое решение, но сделать без двух subscribe не вышло, они так и напрашивались! Если вы сможете подсказать как вы бы это сделали пусть даже для C#, буду благодарен


          1. xGromMx
            16.03.2017 16:27

            вот тут https://jsbin.com/jedarub/1/edit?js,console


            1. Netyaga
              16.03.2017 16:57

              Верно, работает на основе одного subscribe! Правда в RxJava нет оператора mergeMap, попробую сделать подобное на джавовских аналогах


              1. xGromMx
                16.03.2017 17:00

                ну там это flatMap, а тот resultSelector что в mergeMap можно заменить внутренним map


              1. xGromMx
                16.03.2017 17:02

                держи!) https://jsbin.com/jedarub/2/edit?js,console


          1. xGromMx
            16.03.2017 16:29
            +1

            Но вообще надо запомнить, что Rx лучше для асинхронных вещей, а синхронные вещи лучше решать через guava или в scala/kotlin это уже встроено для работы с коллекциями


            1. Netyaga
              16.03.2017 16:54

              Да, тут я с вами согласен, эти примеры с коллекциями придуманы больше для наглядности и понимания операторов


              1. vladsabenin
                17.03.2017 18:20

                Да и тут не сказал бы, что так. Конкретно третий пример — он просто преобразовывал массив и вызвал фукнцию для каждого элемента. Если бы чисто на текучем синтаксисе все реализвоал — было бы куда наглядней. И иэто, если честно, слегка сложно


      1. xGromMx
        16.03.2017 15:39

        Правда я rx юзал только на c# и js. Кстати http://xgrommx.github.io/rx-book/content/resources/articles/index.html#rx


    1. Dethrone
      20.03.2017 23:21

      я начал изучение RxJs из исходников, и очень доволен этой литературой


  1. vladsabenin
    17.03.2017 00:01

    В подспорье автору — больее подходящая реализация третьей задачи на C#


    int count = 98;
    var result = Range(2, count)
       .Where(p => 
           !(Range(2, count).Count(n=>p%n == 0) > 1)
        );


  1. WarKnife
    17.03.2017 10:05
    -1

    Уверен вы решили еще мгновенно


    1. Netyaga
      17.03.2017 10:06

      Благодарю, действительно опечатка, поправил


  1. vba
    17.03.2017 17:30

    По поводу третьей задачи, не совсем понятно зачем там лишние проверки и конверторы, все решается как показано ниже:


    Observable.range(2, 100)
        .filter({isPrime(it)})
        .reduce({s1, s2 -> s1+" "+s2})
        .subscribe({System.out.println it })


    1. Netyaga
      17.03.2017 18:41

      Подскажите, какой Rx вы здесь применили? В RxJava в таком исполнении будут некоторые проблемы:
      Во-первых оператор range реализован в виде

      range(int start, int count)
      
      , то есть будут взяты элементы не с 2 по 100, а с 2 по 101.

      Во-вторых насколько я понимаю оператор reduce принимает на вход функцию
      Func2<T, T, T>
      
      , то есть два входных и выходной объекты должны быть одного типа, то есть подавать на вход int-ы не получится, приходится преобразовать их в String, если мы хотим получить String на выходе

      На счет takeWhile я соглашусь — можно было отказаться от него указав диапазон в range, здесь я оставил такую реализацию чтобы использовать большее разнообразие операторов


  1. Braidner
    19.03.2017 17:35

    Объясните, пожалуйста, чем это отличается от стримов? Единственный плюс, который вижу, это класс Subject, который позваляет добавлять данные в подписку.


    1. Netyaga
      19.03.2017 17:36

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


    1. nobodyhave
      20.03.2017 14:06

      Примеры приведенные автором это только малая часть Rx. Причем именно та часть, которую можно заменить стримами или методами для работы с коллекциями в kotlin/scala. Но, как уже говорилось, они чисто для наглядности.
      Интересности начинаются, когда доходит до Schedulers, Subjects, обработки ошибок и прочих вещей.


  1. vba
    20.03.2017 14:52

    По поводу четвертой задачи, все решается через один оператор reduce, на груви выглядит так:


    def whatDivider = { int n ->
        if (n % 3 == 0) return 3
        if (n % 7 == 0) return 7
        return 0;
    }
    
    Observable.range(1, 20)
        .reduce([:], {Map acc, int x ->
                final n = whatDivider(x)
                if (n == 0) return acc
                acc[n] = (acc[n] ?: []) + x
                acc
            })
        .subscribe({
            it.each{ k, v -> println("Can be divided without remainder by ${k}: ${v}")}
        });

    На джаве должно быть не на много длиннее.


    1. Netyaga
      20.03.2017 18:32

      Благодарю, стоит попробовать импортировать этот метод на джава и избавится от вложенного subscribe


      1. vba
        20.03.2017 18:52

        По идее этот оператор уже доступен в джава версии.