3

Look at these 2 small tests:

@Test
public void test1() {
    Observable.range(1, 10)
        .groupBy(v -> v % 2 == 0)
        .flatMap(group -> {
            if (group.getKey()) {
                return group;
            }
            return group;
        })
        .subscribe(System.out::println);
}

@Test
public void test2() {
    Observable.range(1, 10)
        .groupBy(v -> v % 2 == 0)
        .toMap(g -> g.getKey())
        .flatMapObservable(m -> Observable.merge(
            m.get(true),
            m.get(false)))
        .subscribe(System.out::println);
}

I was expecting both to return a list of numbers in the same order, so:

1 2 3 4 5 6 7 8 9 10

but the second example returns

2 4 6 8 10 1 3 5 7 9

instead.

It seems that on the second example the merge is doing a concat instead, in fact if I change it to a concat, the result is the same.

What am I missing?

Thank you.

Francesco
  • 857
  • 1
  • 11
  • 26

1 Answers1

3

Basically flatMap and merge do not guarantee the order of the emitted items.

From flatMap doc:

Note that FlatMap merges the emissions of these Observables, so that they may interleave.

From merge doc:

Merge may interleave the items emitted by the merged Observables (a similar operator, Concat, does not interleave items, but emits all of each source Observable’s items in turn before beginning to emit items from the next source Observable).

Quote from this SO Answer:

In your case, with single-element, static streams, it is not making any real difference (but in theory, merge could output words in random order and still be valid according to spec)

If you need a guaranteed order use concat* instead.

First example

It works like this:

  • when 1 is emitted the groupBy operator will create a GroupedObservable with key false
    • flatMap will output the items from this observable - which is currently only 1
  • when 2 is emitted the groupBy operator will create a GroupedObservable with key true
    • flatMap will now also output the items from this 2nd GroupedObservable - which is currently 2
  • when 3 is emitted the groupBy operator will add it to the existing GroupedObservable with key false and flatMap will output this item right away
  • when 4 is emitted the groupBy operator will add it to the existing GroupedObservable with key true and flatMap will output this item right away
  • etc.

It may help you to add some more logging:

    Observable.range(1, 10)
            .groupBy(v -> v % 2 == 0)
            .doOnNext(group -> System.out.println("key: " + group.getKey()))
            .flatMap(group -> {
                if (group.getKey()) {
                    return group;
                }
                return group;
            })
            .subscribe(System.out::println);

Then the output is:

key: false
1
key: true
2
3
...

The second example

This is quite different, because toMap will block until the upstream completes:

  • when 1 is emitted the groupBy operator will create a GroupedObservable with key false
    • toMap will add this GroupedObservable to the internal map and uses the key false (the same key as the GroupedObservable has)
  • when 2 is emitted the groupBy operator will create a GroupedObservable with key true
    • toMap will add this GroupedObservable to the internal map and uses the key true (the same key as the GroupedObservable has) - so now the map has 2 GroupedObservables
  • the following numbers are added to the corresponding GroupedObservables and when the source completes, thetoMap operator is done and will pass the map to the next operator
  • in flatMapObservable you use the map to create a new observable where you first add the even elements (key = true) and then the odd elements (key = false)

Also here you could add some more logging:

    Observable.range(1, 10)
            .groupBy(v -> v % 2 == 0)
            .doOnNext(group -> System.out.println("key: " + group.getKey()))
            .toMap(g -> g.getKey())
            .doOnSuccess(map -> System.out.println("map: " + map.size()))
            .flatMapObservable(m -> Observable.merge(
                    m.get(true),
                    m.get(false)
            ))
            .subscribe(System.out::println);

Then the output is:

key: false
key: true
map: 2
2
4
6
8
10
1
3
5
7
9
TmTron
  • 17,012
  • 10
  • 94
  • 142
  • thanks for your answer, but I still don't get why the `merge` inside `flatMapObservable` is not preserving the order. I get now that `toMap` is blocking, as it needs to know the keys of the map in order to emit it for the next stages. `In your case, with single-element, static streams, it is not making any real difference` but the 2 Observable inside the newly map are not single-element, are they? Also I am not sure what `static` means in this context. The definition of `merge` says that elements might interleave, which is what I would expect in this case, instead is behaving like a `concat`. – Francesco Jul 20 '18 at 13:39
  • `in flatMapObservable you use the map to create a new observable where you first add the even elements (key = true) and then the odd elements (key = false)` I think this would be true if I was doing `concat`, but I am doing a `merge` instead. – Francesco Jul 20 '18 at 13:43
  • Right, the 2 Observables in your map have 5 elements each - but they are `static` (i.e. not depending on some timing) - in other words: the streams are already fully defined and all elements are known. `toMap` the source stream has already completed and the Observables in the map can output all their elements immediately when subscribed to. – TmTron Jul 20 '18 at 13:49
  • ad 2nd comment: Well, in this case it is just coincidence that `merge` behaves the same as `concat`. So this `merge` implementation obviously subscribes to the first observable (what you pass as 1st parameter) and processes all its data and then subscribes to the 2nd and processes all data of the 2nd observable. But as mentioned at the top: this is not guaranteed by `merge`. – TmTron Jul 20 '18 at 13:52
  • Well, coincidence may be the wrong word. But what I want to express is, that the behavior of `merge`, that we see is implementation dependent. In other words: other Rx implementations may have a different behavior - or even a newer version of RxJava may have a different behavior. – TmTron Jul 20 '18 at 13:54
  • thanks for your further explanation! clear now :) so `toMap` has quite big implications on the whole stream, as everything must be resolved before going on. I'm now wondering how you can reason with all the groups in one place in RxJava without having to use `toMap` as I am doing in the second example. I posted another question about this if you'd like to have a look :) https://stackoverflow.com/questions/51399925/is-rxjava-a-good-fit-for-branching-workflows – Francesco Jul 20 '18 at 14:03