Runnable: 无输入、无返回值Callable<V>:无输入、有返回值Action:无输入、无返回值,主要用于Rxjava回调Function<T,R>:有输入、有返回值,主要用于数据转换
| 接口 | 典型包 | 参数 | 返回值 | 可声明异常 | 典型用途 |
|---|---|---|---|---|---|
Runnable | java.lang | 无 | 无 | 不能声明受检异常 | 执行一个普通任务 |
Callable<V> | java.util.concurrent | 无 | V | Exception | 执行计算并取得结果 |
Action | RxJava | 无 | 无 | Throwable | 响应完成、取消等事件 |
Function<T,R> | RxJava | 一个 T | R | Throwable | 将一个数据转换成另一个数据 |
1. 同一个业务对比四者
假设有一个加载user的方法:
User loadUser(long userId) throws IOException
Runnable:只执行加载和缓存
long userId = 1001L;
Runnable runnable = () -> {
try {
User user = loadUser(userId);
cacheUser(user);
} catch (IOException e) {
throw new RuntimeException(e);
}
};
Callable:执行加载,并返回User
long userId = 1001L;
Callable<User> callable = () -> {
return loadUser(userId);
};
Action:在RxJava中执行加载和缓存
Action action = () -> {
User user = loadUser(userId);
cacheUser(User);
}
Function:根据传入id返回User
Function<long, User> function = id -> {
return loadUser(id);
}
2. 差异
2.1 Runnable和Action的差异
RxJava中操作经常失败。
假如参数类型是Runnable,必须在lambda中自己包装异常:
Completable.fromRunnable(() -> {
try {
database.saveData();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
而Action的方法声明支持throws Throwable,RxJava可以捕获异常并转换为onError:
Completable.fromAction(() -> {
database.saveData();
})
.subscribe(
() -> log("成功"),
error -> log("失败:" + error)
);
RxJava 1 的 Func 与 Action
RxJava 1 里的 Func0/Func1/Func2... 和 Action0/Action1/Action2... 都是函数式接口,用来承接回调逻辑。
核心区别:
Func有返回值,Action没有返回值。
命名规则
数字表示参数个数。
| 类型 | 参数 | 返回值 | 含义 |
|---|---|---|---|
Func0<R> | 0 个 | 有 | 无参,返回 R |
Func1<T, R> | 1 个 | 有 | 输入 T,返回 R |
Func2<T1, T2, R> | 2 个 | 有 | 输入 T1、T2,返回 R |
Action0 | 0 个 | 无 | 无参,只执行动作 |
Action1<T> | 1 个 | 无 | 消费一个 T,只执行动作 |
Action2<T1, T2> | 2 个 | 无 | 消费两个参数,只执行动作 |
可以简单记成:
Func:function,需要把结果交给下游。Action:action,只做副作用,不产生新值。
call() 的含义
call() 是 RxJava 1 这些函数式接口约定的执行入口。
Func1<T, R> 可以近似理解成:
public interface Func1<T, R> {
R call(T t);
}所以这段代码:
new Func1<String, Boolean>() {
@Override
public Boolean call(String value) {
return value.length() > 0;
}
}含义是:创建一个”接收 String,返回 Boolean”的函数对象。RxJava 在需要执行这个函数时,会调用它的 call() 方法。
例如:
Observable.just("a", "", "b")
.filter(new Func1<String, Boolean>() {
@Override
public Boolean call(String value) {
return value.length() > 0;
}
});执行过程可以理解为:
上游发 "a" -> call("a") -> true -> 保留
上游发 "" -> call("") -> false -> 过滤掉
上游发 "b" -> call("b") -> true -> 保留不同操作符里,call() 返回值的意义不同:
map:返回转换后的新值。filter:返回是否保留当前值。flatMap:返回新的Observable。zip/combineLatest:返回合并后的结果。
Func 示例
Func1 常见于 map、flatMap、filter 这类需要返回结果的操作。
map 需要把 Integer 转成 String:
Observable.just(1, 2, 3)
.map(new Func1<Integer, String>() {
@Override
public String call(Integer value) {
return "num=" + value;
}
});filter 也用 Func1,因为它要返回 Boolean,告诉上游这个值是否保留:
Observable.just(1, 2, 3)
.filter(new Func1<Integer, Boolean>() {
@Override
public Boolean call(Integer value) {
return value > 1;
}
});Func2 常见于 zip、combineLatest:
Observable.combineLatest(
nameObservable,
ageObservable,
new Func2<String, Integer, String>() {
@Override
public String call(String name, Integer age) {
return name + ":" + age;
}
});这里必须返回合并后的结果,所以是 Func2。
Action 示例
Action 常见于 subscribe、doOnNext、doOnCompleted 这类只执行动作的地方。
Action1 用来消费 onNext 数据:
Observable.just("a", "b")
.subscribe(new Action1<String>() {
@Override
public void call(String value) {
LogUtils.d(value);
}
});这里只是打印日志,不把新值传给下游,所以没有返回值。
Action0 常见于完成回调:
observable.doOnCompleted(new Action0() {
@Override
public void call() {
LogUtils.d("completed");
}
});Action2 用于两个参数的副作用回调。例如项目里有些图片加载、marker 渲染回调会设计成”拿到 bitmap 和业务对象后执行某个动作”,这种场景不需要返回值,就适合 Action2<T1, T2>。
怎么判断该用哪个
看调用方是否需要你返回一个值。
需要返回值时,用 Func:
map: T -> R
filter: T -> Boolean
flatMap: T -> Observable<R>
zip/combineLatest: (T1, T2) -> R只执行副作用时,用 Action:
subscribe(onNext): T -> void
doOnNext: T -> void
doOnCompleted: () -> void
doOnError: Throwable -> void和 lambda 的对应关系
老代码里经常看到匿名类:
new Action1<User>() {
@Override
public void call(User user) {
refresh(user);
}
}用 lambda 写就是:
user -> refresh(user)如果调用位置是 subscribe(...) 或 doOnNext(...),这个 lambda 通常对应 Action1<T>。
如果调用位置是 map(...) 或 filter(...),这个 lambda 通常对应 Func1<T, R>:
.map(user -> user.name)
.filter(user -> user != null)lambda 类型推断失败时为什么用匿名类
lambda 本身没有独立类型,它的参数类型要靠调用上下文推断。
比如:
.filter(value -> value.length() > 0)编译器必须先知道 value 是 String,才能确认 length() 合法。如果上游泛型丢了,比如链路变成了原始类型、Observable<Object>,或者某个中间方法没有保留泛型,编译器可能把 value 推断成 Object。这时就会出现类似问题:
Cannot resolve method length()匿名类能解决一部分这类问题,是因为它显式写出了函数接口的泛型:
.filter(new Func1<String, Boolean>() {
@Override
public Boolean call(String value) {
return value.length() > 0;
}
})这里 new Func1<String, Boolean>() 明确告诉编译器:
输入类型是 String
返回类型是 Boolean所以 call(String value) 里的 value 不再靠 lambda 自动推断,而是明确的 String。
不过要注意:匿名类不是万能修复。如果上游真实类型就是 Observable<Object>,直接传 Func1<String, Boolean> 可能仍然类型不兼容。更稳的优先级是:
- 先修正上游泛型,让链路变成
Observable<String>。 - 泛型不好改时,再用匿名类显式指定
Func1<String, Boolean>。 - 必要时在链路里明确转换类型。
例如:
Observable<String> observable = ...
observable.filter(value -> value.length() > 0);或者:
observable
.map(obj -> (String) obj)
.filter(value -> value.length() > 0);所以更准确的说法是:不是”推断错了就必须用匿名类”,而是”匿名类可以显式声明泛型,从而绕开 lambda 的类型推断问题”。根因通常还是上游泛型信息丢失。
项目里的直觉
订阅回调通常是 Action 思路:
.subscribe(Rxu.ob(
unit -> {
// onNext,只消费事件
},
e -> {
// onError,只处理异常
}))转换链路通常是 Func 思路:
.map(item -> transform(item))
.filter(item -> check(item))前者关注”发生后做什么”,后者关注”把数据变成什么”。
RxJava 2 之后的变化
RxJava 2 以后命名更接近 Java 8 函数接口:
| RxJava 1 | RxJava 2/3 近似对应 |
|---|---|
Func1<T, R> | Function<T, R> |
Func2<T1, T2, R> | BiFunction<T1, T2, R> |
Action1<T> | Consumer<T> |
Action0 | Action |
所以在 RxJava 1 代码里看到 FuncX 和 ActionX 时,先看有没有返回值,就能快速判断它的角色。
总结
Func 是”输入参数,返回结果”的函数;Action 是”输入参数,只做事情”的动作。
在 RxJava 链路里:
- 数据转换、条件判断、合并结果:优先想到
Func。 - 打日志、更新 UI、写缓存、订阅消费:优先想到
Action。