• Runnable: 无输入、无返回值
  • Callable<V>:无输入、有返回值
  • Action:无输入、无返回值,主要用于Rxjava回调
  • Function<T,R>:有输入、有返回值,主要用于数据转换
接口典型包参数返回值可声明异常典型用途
Runnablejava.lang不能声明受检异常执行一个普通任务
Callable<V>java.util.concurrentVException执行计算并取得结果
ActionRxJavaThrowable响应完成、取消等事件
Function<T,R>RxJava一个 TRThrowable将一个数据转换成另一个数据

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 RunnableAction的差异

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 个输入 T1T2,返回 R
Action00 个无参,只执行动作
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 常见于 mapflatMapfilter 这类需要返回结果的操作。

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 常见于 zipcombineLatest

Observable.combineLatest(
    nameObservable,
    ageObservable,
    new Func2<String, Integer, String>() {
      @Override
      public String call(String name, Integer age) {
        return name + ":" + age;
      }
    });

这里必须返回合并后的结果,所以是 Func2

Action 示例

Action 常见于 subscribedoOnNextdoOnCompleted 这类只执行动作的地方。

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)

编译器必须先知道 valueString,才能确认 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> 可能仍然类型不兼容。更稳的优先级是:

  1. 先修正上游泛型,让链路变成 Observable<String>
  2. 泛型不好改时,再用匿名类显式指定 Func1<String, Boolean>
  3. 必要时在链路里明确转换类型。

例如:

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 1RxJava 2/3 近似对应
Func1<T, R>Function<T, R>
Func2<T1, T2, R>BiFunction<T1, T2, R>
Action1<T>Consumer<T>
Action0Action

所以在 RxJava 1 代码里看到 FuncXActionX 时,先看有没有返回值,就能快速判断它的角色。

总结

Func 是”输入参数,返回结果”的函数;Action 是”输入参数,只做事情”的动作。

在 RxJava 链路里:

  • 数据转换、条件判断、合并结果:优先想到 Func
  • 打日志、更新 UI、写缓存、订阅消费:优先想到 Action