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

у меня есть ситуация, когда я создаю наблюдаемый, содержащий результаты из базы данных. Затем я применяю к ним ряд фильтров. Затем у меня есть подписчик, который регистрирует результаты. Это может быть так, что никакие элементы не делают свой путь через фильтры. Моя бизнес-логика утверждает, что это не ошибка. Однако, когда это происходит, вызывается мой onError и содержит следующее исключение:java.util.NoSuchElementException: Sequence contains no elements

принятая практика для того чтобы как раз обнаружить этот тип исключение и игнорировать его? Или есть лучший способ справиться с этим?

версия 1.0.0.

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

  @Test
public void test()
{

    Integer values[] = new Integer[]{1, 2, 3, 4, 5};

    Observable.from(values).filter(new Func1<Integer, Boolean>()
    {
        @Override
        public Boolean call(Integer integer)
        {
            if (integer < 0)
                return true;
            else
                return false;
        }
    }).map(new Func1<Integer, String>()
    {
        @Override
        public String call(Integer integer)
        {
            return String.valueOf(integer);
        }
    }).reduce(new Func2<String, String, String>()
    {
        @Override
        public String call(String s, String s2)
        {
            return s + "," + s2;
        }
    })

            .subscribe(new Action1<String>()
            {
                @Override
                public void call(String s)
                {
                    System.out.println(s);
                }
            });
}

поскольку я использую безопасный подписчик, он изначально создает исключение OnErrorNotImplementedException, которое обертывает следующее исключение:

java.util.NoSuchElementException: Sequence contains no elements
    at rx.internal.operators.OperatorSingle.onCompleted(OperatorSingle.java:82)
    at rx.internal.operators.NotificationLite.accept(NotificationLite.java:140)
    at rx.internal.operators.TakeLastQueueProducer.emit(TakeLastQueueProducer.java:73)
    at rx.internal.operators.TakeLastQueueProducer.startEmitting(TakeLastQueueProducer.java:45)
    at rx.internal.operators.OperatorTakeLast.onCompleted(OperatorTakeLast.java:59)
    at rx.internal.operators.OperatorScan.onCompleted(OperatorScan.java:121)
    at rx.internal.operators.OperatorMap.onCompleted(OperatorMap.java:43)
    at rx.internal.operators.OperatorFilter.onCompleted(OperatorFilter.java:42)
    at rx.internal.operators.OnSubscribeFromIterable$IterableProducer.request(OnSubscribeFromIterable.java:79)
    at rx.internal.operators.OperatorScan.request(OperatorScan.java:147)
    at rx.Subscriber.setProducer(Subscriber.java:139)
    at rx.internal.operators.OperatorScan.setProducer(OperatorScan.java:139)
    at rx.Subscriber.setProducer(Subscriber.java:133)
    at rx.Subscriber.setProducer(Subscriber.java:133)
    at rx.internal.operators.OnSubscribeFromIterable.call(OnSubscribeFromIterable.java:47)
    at rx.internal.operators.OnSubscribeFromIterable.call(OnSubscribeFromIterable.java:33)
    at rx.Observable.call(Observable.java:144)
    at rx.Observable.call(Observable.java:136)
    at rx.Observable.call(Observable.java:144)
    at rx.Observable.call(Observable.java:136)
    at rx.Observable.call(Observable.java:144)
    at rx.Observable.call(Observable.java:136)
    at rx.Observable.call(Observable.java:144)
    at rx.Observable.call(Observable.java:136)
    at rx.Observable.call(Observable.java:144)
    at rx.Observable.call(Observable.java:136)
    at rx.Observable.subscribe(Observable.java:7284)

на основе ответа от @davem ниже, я создал новый тестовый случай:

@Test
public void testFromBlockingAndSingle()
{

    Integer values[] = new Integer[]{-2, -1, 0, 1, 2, 3, 4, 5};

    List<String> results = Observable.from(values).filter(new Func1<Integer, Boolean>()
    {
        @Override
        public Boolean call(Integer integer)
        {
            if (integer < 0)
                return true;
            else
                return false;
        }
    }).map(new Func1<Integer, String>()
    {
        @Override
        public String call(Integer integer)
        {
            return String.valueOf(integer);
        }
    }).reduce(new Func2<String, String, String>()
    {
        @Override
        public String call(String s, String s2)
        {
            return s + "," + s2;
        }
    }).toList().toBlocking().single();

    System.out.println("Test: " + results + " Size: " + results.size());

}

и этот тест приводит к следующему поведению:

когда вход:

Integer values[] = new Integer[]{-2, -1, 0, 1, 2, 3, 4, 5};

тогда результаты (как и ожидалось):

Test: [-2,-1] Size: 1

и когда входные данные:

Integer values[] = new Integer[]{0, 1, 2, 3, 4, 5};

тогда результатом является следующая трассировка стека:

java.util.NoSuchElementException: Sequence contains no elements
at rx.internal.operators.OperatorSingle.onCompleted(OperatorSingle.java:82)
at rx.internal.operators.NotificationLite.accept(NotificationLite.java:140)
at rx.internal.operators.TakeLastQueueProducer.emit(TakeLastQueueProducer.java:73)
at rx.internal.operators.TakeLastQueueProducer.startEmitting(TakeLastQueueProducer.java:45)
at rx.internal.operators.OperatorTakeLast.onCompleted(OperatorTakeLast.java:59)
at rx.internal.operators.OperatorScan.onCompleted(OperatorScan.java:121)
at rx.internal.operators.OperatorMap.onCompleted(OperatorMap.java:43)
at rx.internal.operators.OperatorFilter.onCompleted(OperatorFilter.java:42)
at rx.internal.operators.OnSubscribeFromIterable$IterableProducer.request(OnSubscribeFromIterable.java:79)
at rx.internal.operators.OperatorScan.request(OperatorScan.java:147)
at rx.Subscriber.setProducer(Subscriber.java:139)
at rx.internal.operators.OperatorScan.setProducer(OperatorScan.java:139)
at rx.Subscriber.setProducer(Subscriber.java:133)
at rx.Subscriber.setProducer(Subscriber.java:133)
at rx.internal.operators.OnSubscribeFromIterable.call(OnSubscribeFromIterable.java:47)
at rx.internal.operators.OnSubscribeFromIterable.call(OnSubscribeFromIterable.java:33)
at rx.Observable.call(Observable.java:144)
at rx.Observable.call(Observable.java:136)
at rx.Observable.call(Observable.java:144)
at rx.Observable.call(Observable.java:136)
at rx.Observable.call(Observable.java:144)
at rx.Observable.call(Observable.java:136)
at rx.Observable.call(Observable.java:144)
at rx.Observable.call(Observable.java:136)
at rx.Observable.call(Observable.java:144)
at rx.Observable.call(Observable.java:136)
at rx.Observable.call(Observable.java:144)
at rx.Observable.call(Observable.java:136)
at rx.Observable.call(Observable.java:144)
at rx.Observable.call(Observable.java:136)
at rx.Observable.subscribe(Observable.java:7284)
at rx.observables.BlockingObservable.blockForSingle(BlockingObservable.java:441)
at rx.observables.BlockingObservable.single(BlockingObservable.java:340)
at EmptyTest2.test(EmptyTest2.java:19)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner.run(ParentRunner.java:238)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
at org.junit.runners.ParentRunner.access0(ParentRunner.java:53)
at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
at org.junit.runner.JUnitCore.run(JUnitCore.java:160)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:74)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:211)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:67)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

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

@Test
public void testNoReduce()
{

    Integer values[] = new Integer[]{-2, -1, 0, 1, 2, 3, 4, 5};

    List<String> results = Observable.from(values).filter(new Func1<Integer, Boolean>()
    {
        @Override
        public Boolean call(Integer integer)
        {
            if (integer < 0)
                return true;
            else
                return false;
        }
    }).map(new Func1<Integer, String>()
    {
        @Override
        public String call(Integer integer)
        {
            return String.valueOf(integer);
        }
    }).toList().toBlocking().first();

    Iterator<String> itr = results.iterator();
    StringBuilder b = new StringBuilder();

    while (itr.hasNext())
    {
        b.append(itr.next());

        if (itr.hasNext())
            b.append(",");
    }

    System.out.println("Test NoReduce: " + b);

}

со следующим вводом:

Integer values[] = new Integer[]{-2, -1, 0, 1, 2, 3, 4, 5};

я получаю следующие результаты, которые ожидается:

Test NoReduce: -2,-1

и с следующим образом:

Integer values[] = new Integer[]{0, 1, 2, 3, 4, 5};

я получаю следующий результат, который ожидается:

Test NoReduce: 

Итак, если я не полностью не понимаю что-то, единственный способ действительно обрабатывать нулевой элемент, наблюдаемый в результате фильтра, когда за ним следует карта и уменьшить, - это реализовать логика сокращения вне наблюдаемой цепи. Вы все согласны с этим утверждением?


Окончательное Решение

вот мое окончательное решение после реализации того, что предложили Томаш Дворжак и Дэвид Моттен. Я думаю, что это разумное решение.

@Test
public void testWithToList()
{

    Integer values[] = new Integer[]{-2, -1, 0, 1, 2, 3, 4, 5};

     Observable.from(values).filter(new Func1<Integer, Boolean>()
     {
         @Override
         public Boolean call(Integer integer)
         {
             if (integer < 0)
                 return true;
             else
                 return false;
         }
     }).toList().map(new Func1<List<Integer>, String>()
     {
         @Override
         public String call(List<Integer> integers)
         {
             Iterator<Integer> intItr = integers.iterator();
             StringBuilder b = new StringBuilder();

             while (intItr.hasNext())
             {
                 b.append(intItr.next());

                 if (intItr.hasNext())
                 {
                     b.append(",");
                 }
             }

             return b.toString();
         }
     }).subscribe(new Action1<String>()
     {
         @Override
         public void call(String s)
         {
             System.out.println("With a toList: " + s);
         }
     });

}

вот как этот тест ведет себя при следующих входных данных.

, когда данный поток, который будет иметь некоторые значения проходят через фильтры:

Integer values[] = new Integer[]{-2, -1, 0, 1, 2, 3, 4, 5};

результат:

With a toList: -2,-1

когда задан поток, который не будет иметь никаких значений, проходят через фильтры:

Integer values[] = new Integer[]{0, 1, 2, 3, 4, 5};

результат:

With a toList: <empty string>

3 ответов


теперь после обновления ошибка вполне очевидна. Reduce в RxJava потерпит неудачу с IllegalArgumentException Если наблюдаемое оно уменьшает пусто, то точно согласно спецификации (http://reactivex.io/documentation/operators/reduce.html).

в функциональном программировании обычно есть два универсальных оператора, которые объединяют коллекцию в одно значение,fold и reduce. В принятой терминологии, fold принимает начальное значение аккумулятора, и a функция, которая принимает работающий аккумулятор и значение из коллекции и создает другое значение аккумулятора. Пример в псевдокоде:

[1, 2, 3, 4].fold(0, (accumulator, value) => accumulator + value)

начнется с 0 и в конечном итоге добавит 1, 2, 3, 4 к работающему аккумулятору, наконец, получив 10, сумму значений.

Reduce очень похоже, только он не принимает начальное значение аккумулятора явно, он использует первое значение в качестве начального аккумулятора, а затем накапливает все остаточная стоимость. Это имеет смысл, если вы, например, ищете минимальное или максимальное значение.

[1, 2, 3, 4].reduce((accumulator, value) => min(accumulator, value))

глядя на сгиб и уменьшить по-разному, вы, вероятно, будете использовать foldвсякий раз, когда агрегированное значение будет иметь смысл даже на пустой коллекции (например, в sum, 0 имеет смысл), и reduce в противном случае (minimum не имеет смысла на пустой коллекции, и reduce не сумеет работать на таком собрании, в вашем случае путем бросать исключение.)

вы выполняете аналогичную агрегацию, чередуя коллекцию строк с запятой для создания одной строки. Это немного более сложная ситуация. Вероятно, это имеет смысл в пустой коллекции (вы, вероятно, ожидаете пустую строку), но, с другой стороны, если вы начнете с пустого аккумулятора, у вас будет еще одна запятая в результате, чем вы ожидаете. Правильное решение для этого-сначала проверить, пуста ли коллекция, и либо вернуть строка отката для пустой коллекции или do a reduce на непустой коллекции. Вероятно, вы заметите, что часто вам на самом деле не нужна пустая строка в пустом случае коллекции, но что-то вроде "коллекция пуста" может быть более подходящим, тем самым еще раз заверяя вас, что это решение является чистым.

кстати, я использую слово коллекция здесь вместо наблюдаемых свободно, как раз для воспитательных целей. Кроме того, в RxJava, both fold и reduce называются то же самое,reduce, только есть две версии этого метода, одна из которых принимает только один параметр, другие два параметра.

что касается вашего последнего вопроса: вам не нужно покидать наблюдаемую цепочку. Просто используйте toList (), как предлагает Дэвид Моттен.

.filter(...)
.toList()
.map(listOfValues => listOfValues.intersperse(", "))

здесь intersperse может быть реализовано в терминах reduce, если еще не библиотечная функция (она довольно распространена).

collection.intersperse(separator) = 
    if (collection.isEmpty()) 
      ""
    else
      collection.reduce(accumulator, element => accumulator + separator + element)

причина этого происходит в том, что вы используете toBlocking().single() на пустой поток. Если вы ожидаете 0 или 1 значения из потока, вы можете сделать toList().toBlocking().single() затем проверьте значения в списке (которые могут быть пустыми, но не будут провоцировать исключение, которое вы получаете).


вы можете использовать reduce с начальным значением version .reduce(0, (x,y) -> x+y ) Он должен работать как оператор сгиба, объясненный Tomáš Dvořák