在 RxJava 中,何时使用 map 和 flatMap?

196

RxJava中,何时使用mapflatMap

例如,我们想将包含JSON的文件映射为包含该JSON的字符串——

使用map时,必须处理Exception。但是如何处理呢?:

Observable.from(jsonFile).map(new Func1<File, String>() {
    @Override public String call(File file) {
        try {
            return new Gson().toJson(new FileReader(file), Object.class);
        } catch (FileNotFoundException e) {
            // So Exception. What to do ?
        }
        return null; // Not good :(
    }
});

使用 flatMap,虽然更加冗长,但我们可以将问题向下传递到链中的 Observables 并在其他地方处理错误,甚至可以重试:

Observable.from(jsonFile).flatMap(new Func1<File, Observable<String>>() {
    @Override public Observable<String> call(final File file) {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override public void call(Subscriber<? super String> subscriber) {
                try {
                    String json = new Gson().toJson(new FileReader(file), Object.class);

                    subscriber.onNext(json);
                    subscriber.onCompleted();
                } catch (FileNotFoundException e) {
                    subscriber.onError(e);
                }
            }
        });
    }
});

我喜欢map的简洁性,但是flatmap的错误处理更好(并不是因为它更啰嗦)。我还没有看到任何关于此的最佳实践,我很好奇实际上这是如何被使用的。

11个回答

130

map将一个事件转换为另一个事件。flatMap将一个事件转换为零个或多个事件。(这是来自IntroToRx的内容)

如果您想将JSON转换为对象,使用map就足够了。

处理FileNotFoundException是另一个问题(使用map或flatMap无法解决此问题)。

要解决异常问题,只需使用非检查异常抛出它:RX会为您调用onError处理程序。

Observable.from(jsonFile).map(new Func1<File, String>() {
    @Override public String call(File file) {
        try {
            return new Gson().toJson(new FileReader(file), Object.class);
        } catch (FileNotFoundException e) {
            // this exception is a part of rx-java
            throw OnErrorThrowable.addValueAsLastCause(e, file);
        }
    }
});

使用flatmap的完全相同版本:

Observable.from(jsonFile).flatMap(new Func1<File, Observable<String>>() {
    @Override public Observable<String> call(File file) {
        try {
            return Observable.just(new Gson().toJson(new FileReader(file), Object.class));
        } catch (FileNotFoundException e) {
            // this static method is a part of rx-java. It will return an exception which is associated to the value.
            throw OnErrorThrowable.addValueAsLastCause(e, file);
            // alternatively, you can return Obersable.empty(); instead of throwing exception
        }
    }
});

在flatMap版本中,您也可以返回一个新的Observable,该Observable只是一个错误。

Observable.from(jsonFile).flatMap(new Func1<File, Observable<String>>() {
    @Override public Observable<String> call(File file) {
        try {
            return Observable.just(new Gson().toJson(new FileReader(file), Object.class));
        } catch (FileNotFoundException e) {
            return Observable.error(OnErrorThrowable.addValueAsLastCause(e, file));
        }
    }
});

2
这不会调用 subscriber.onError() 等。我看到的所有示例都是通过这种方式路由错误。那不重要吗? - Christopher Perry
7
请注意,OnErrorThrowable的构造函数是private的,您需要使用OnErrorThrowable.from(e)代替。 - david.mihola
我刚刚更新了。OnErrorThrowable.from(e) 不会保留值,所以我使用 OnErrorThrowable.addValueAsLastCause(e, file) 代替它,这样应该可以保留值。 - dwursteisen
1
我喜欢这些代码示例,但如果您更新flatMap调用的签名来返回Observable<String>而不仅仅是String,这将会有所帮助...因为这不是它们之间的技术差异吗? - Rich Ehmer

83
FlatMap的行为与map非常相似,不同之处在于它应用的函数本身返回一个observable,因此它非常适合映射异步操作。
在实际意义上,函数Map仅对链接响应进行转换(不返回Observable);而函数FlatMap应用返回一个Observable<T>,这就是为什么如果您计划在方法中进行异步调用,则建议使用FlatMap。
总结:
- Map返回类型为T的对象 - FlatMap返回一个Observable。
可以在此处查看明确的示例:http://blog.couchbase.com/why-couchbase-chose-rxjava-new-java-sdk
Couchbase Java 2.X客户端使用Rx以方便的方式提供异步调用。由于它使用Rx,因此具有map和FlatMap方法,其文档中的解释可能有助于理解一般概念。
要处理错误,请在您的订阅者上覆盖onError。
Subscriber<String> mySubscriber = new Subscriber<String>() {
    @Override
    public void onNext(String s) { System.out.println(s); }

    @Override
    public void onCompleted() { }

    @Override
    public void onError(Throwable e) { }
};

可能会对您有所帮助,查看此文档:http://blog.danlew.net/2014/09/15/grokking-rxjava-part-1/

关于如何使用RX处理错误的良好资源可以在此找到: https://gist.github.com/daschl/db9fcc9d2b932115b679


2
总结是错误的。Map和FlatMap返回相同的类型,但它们应用的函数返回不同的类型。 - CoXier

69

根据您的情况,您需要使用 map,因为只有一个输入和一个输出。

map - 提供的函数只接受一个项目并返回一个项目,该项目将进一步发出(仅一次)。

flatMap - 提供的函数接受一个项目,然后返回一个“Observable”,这意味着新“Observable”的每个项目都将单独发出。

也许代码会让您清楚明白:

Observable.just("item1").map( str -> {
    System.out.println("inside the map " + str);
    return str;
}).subscribe(System.out::println);

Observable.just("item2").flatMap( str -> {
    System.out.println("inside the flatMap " + str);
    return Observable.just(str + "+", str + "++" , str + "+++");
}).subscribe(System.out::println);

输出:

inside the map item1
item1
inside the flatMap item2
item2+
item2++
item2+++

不确定使用 Map 是否是最佳选择,虽然它可以工作。假设 FileReader 变为异步调用。那么你需要将 map 改为 flatMap。如果保留为 map,则意味着您不会按预期触发事件,并且会导致混乱。我已经在学习 RX Java 的过程中遇到过这个问题。我发现 flatMap 是确保事情按照预期处理的可靠方法。 - KRK Owner

42

问题是在RxJava中何时使用map与flatMap?我认为一个简单的演示更具体。

当你想要将发出的项目转换为另一种类型时,比如将文件转换为字符串,map和flatMap都可以工作。但是我更喜欢map操作符,因为它更清晰明了。

然而,在某些情况下,flatMap可以发挥神奇的作用,而map则不能。例如,当我想要获取用户的信息,但必须先获取用户登录时的ID时。显然,我需要两个请求,并且它们是按顺序进行的。

让我们开始吧。

Observable<LoginResponse> login(String email, String password);

Observable<UserInfo> fetchUserInfo(String userId);

这里有两种方法,一种是返回Response的登录方法,另一种是获取用户信息的方法。

login(email, password)
        .flatMap(response ->
                fetchUserInfo(response.id))
        .subscribe(userInfo -> {
            // get user info and you update ui now
        });

正如你所看到的,在flatMap函数中,首先我从Response中获取用户id,然后获取用户信息。当两个请求完成后,我们就可以进行我们的工作,比如更新UI或将数据保存到数据库中。

但是如果你使用map,你就不能写出这样好的代码。总之,flatMap函数可以帮助我们串行化请求。


29
我认为,当你想要放入map()中的函数返回一个Observable时,你需要使用flatMap。在这种情况下,仍然可以尝试使用map(),但是这将不切实际。让我试着解释一下为什么。
如果在这种情况下你决定坚持使用map,你将得到一个Observable<Observable<Something>>。例如,在你的情况下,如果我们使用一个想象中的 RxGson 库,它从其toJson()方法返回一个Observable<String>(而不仅仅是返回一个String),那么它看起来像这样:
Observable.from(jsonFile).map(new Func1<File, Observable<String>>() {
    @Override public Observable<String>> call(File file) {
        return new RxGson().toJson(new FileReader(file), Object.class);
    }
}); // you get Observable<Observable<String>> here

目前,要对这样的observable对象进行subscribe()是相当棘手的。因为你会得到一个Observable<String>,你需要再一次进行subscribe()才能获得结果值,这不仅不实用,而且难以接受。

因此,让它变得有用的一个想法是“展平”这个observable对象的observable。(你可能已经开始看出_flat_Map名称的由来了)。 RxJava提供了几种展平observables的方法,为了简单起见,让我们假设merge是我们想要的方法。Merge基本上需要一堆observables,并在其中任何observable发出后立即发出。 (许多人会认为switch是更好的默认选择,但如果你只发出一个值,无论如何都没有关系。)

因此,修改我们之前的代码片段,我们将得到:

Observable.from(jsonFile).map(new Func1<File, Observable<String>>() {
    @Override public Observable<String>> call(File file) {
        return new RxGson().toJson(new FileReader(file), Object.class);
    }
}).merge(); // you get Observable<String> here

这个操作更有用,因为订阅它(或映射、过滤等等),你只会得到String值。(需要注意的是,在RxJava中不存在这种变体的merge(),但如果您理解了合并的想法,我希望您也能理解如何工作。)

所以基本上由于这样的merge()可能只在成功返回一个可观察对象的map()之后有用,所以创建了flatMap()作为一种简写方式。它应用映射函数就像普通的map()一样,但稍后不仅发出返回的值,还将它们“展平”(或合并)。

这是通常的用例。它在使用Rx的所有地方和您要链接其他返回observable的方法的代码库中最有用。

在您的用例中,它同样有用,因为map()只能将在onNext()中发出的一个值转换为在onNext()中发出的另一个值。但它无法将其转换为多个值、没有值或错误。正如akarnokd在他的回答中所写的那样(请注意,他比我聪明得多,也许总体来说是这样,但至少在RxJava方面是这样),您不应该从map()中抛出异常。因此,您可以使用flatMap()

return Observable.just(value);

一切顺利时,但是...

return Observable.error(exception);

当某些事情失败时。
请查看他的答案以获取完整代码片段:https://dev59.com/_GAh5IYBdhLWcg3wE_1P#30330772


1
这是我喜欢的答案。如果你的方法返回的是一个可观察的对象,那么你基本上会将一个可观察对象嵌套在另一个可观察对象中。 - filthy_wizard

24

这里有一个简单的“拇指规则”,我用它来决定在 Rx 的 Observable 中何时使用 flatMap() 而不是 map()

一旦你决定使用 map 转换,你会编写转换代码以返回某个对象,对吧?

如果你的转换结果是:

  • 非 Observable 对象,则只需使用 map()。而 map() 会将该对象包装在 Observable 中并发出它。

  • Observable 对象,则应使用 flatMap()。而 flatMap() 会解开 Observable,选择返回的对象,用自己的 Observable 包装它并发出它。

例如,我们有一个方法 titleCase(String inputParam),它返回输入参数的标题大小写字符串对象。此方法的返回类型可以是 StringObservable<String>

  • 如果 titleCase(..) 的返回类型仅为 String,则应使用 map(s -> titleCase(s))

  • 如果 titleCase(..) 的返回类型为 Observable<String>,则应使用 flatMap(s -> titleCase(s))

希望这能澄清问题。


13

我只想补充一点,使用flatMap时,你不需要在函数内部使用自定义的Observable,而可以依赖于标准的工厂方法/操作符:

Observable.from(jsonFile).flatMap(new Func1<File, Observable<String>>() {
    @Override public Observable<String> call(final File file) {
        try {
            String json = new Gson().toJson(new FileReader(file), Object.class);
            return Observable.just(json);
        } catch (FileNotFoundException ex) {
            return Observable.<String>error(ex);
        }
    }
});

通常情况下,应该尽可能避免从onXXX方法和回调中抛出(运行时)异常,即使我们在RxJava中尽可能地添加了多重保障。


但我认为map就足够了。所以flatMap和map是一种习惯,对吗? - CoXier

8
在这种情况下,请使用map,您不需要为此创建新的Observable。
在这种情况下,您应该使用Exceptions.propagate,这是一个包装器,因此您可以将这些已检查的异常发送到rx机制。
Observable<String> obs = Observable.from(jsonFile).map(new Func1<File, String>() { 
    @Override public String call(File file) {
        try { 
            return new Gson().toJson(new FileReader(file), Object.class);
        } catch (FileNotFoundException e) {
            throw Exceptions.propagate(t); /will propagate it as error
        } 
    } 
});

您需要在订阅者中处理此错误。
obs.subscribe(new Subscriber<String>() {
    @Override 
    public void onNext(String s) { //valid result }

    @Override 
    public void onCompleted() { } 

    @Override 
    public void onError(Throwable e) { //e might be the FileNotFoundException you got }
};); 

这里有一篇非常优秀的关于it技术的文章:http://blog.danlew.net/2015/12/08/error-handling-in-rxjava/


4

RxJava中的 Map 和 FlatMap

它们都是转换操作符,但是 map 是一对一的关系,而 flatMap 则是一对零或多的关系。

  • map flatmap 发出,其中
    • map - 只有1个元素
    • flatmap - 0或多个元素
  • map 发出单个元素,而 flatmap 则发出一个的元素

Map 运算符

map(new Function<A, B>() {
    @Override
    public B apply(A a) throws Exception {
        B b = new B(a);
        return b;
    }
})

FlatMap运算符

flatMap(new Function<A, ObservableSource<B>>() { 
    @Override
    public ObservableSource<B> apply(A a) throws Exception {
        return foo(a);
    }
})

[flatMap与concatMap的区别]

[Swift中map和flatMap的区别]


1

在某些情况下,您可能最终拥有一系列的 observables,其中您的 observable 将返回另一个 observable。'flatmap' 会解开嵌在第一个 observable 中的第二个 observable,并允许您在订阅时直接访问第二个 observable 输出的数据。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接