Правильная обработка пустого наблюдаемого в 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