在 Android、Swing 中,经常会看到用匿名内部类处理事件的代码。比如 Android 中的按钮点击事件:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this, "you have clicked Button",
                Toast.LENGTH_SHORT).show();
    }
});

为了传递一段点击后的处理逻辑,还要写接口名、方法名和参数声明,样板代码不少。

Java 8 引入了 Lambda 表达式。在支持 Java 8 Lambda 的 Android 项目中,上面可以写成:

button.setOnClickListener(v ->
        Toast.makeText(MainActivity.this, "you have clicked Button",
                Toast.LENGTH_SHORT).show());

v 是参数名,对应 onClick 的参数;-> 后面就是点击时要执行的代码。这里的目标接口是 View.OnClickListener,Lambda 提供了它的 onClick 方法实现。

Lambda 的几种写法

先看不依赖 Android 的例子。Runnable 的 run() 没有参数,用空括号 () 表示;方法体有多条语句时,用大括号括起来:

Runnable runnable = () -> {
    System.out.println("hello!");
    System.out.println("run!");
};
runnable.run();

只有一个参数,并且省略参数类型时,可以连参数外面的括号一起省略:

Consumer<String> printer = text -> System.out.println(text);
printer.accept("hello lambda");

这里的 Consumer 来自 java.util.function 包。Lambda 的主体可以是一个表达式,也可以是大括号包住的代码块;是否需要返回值,要看对应接口的方法。

多个参数需要括号。例如两个 Long 相加:

BinaryOperator<Long> add = (x, y) -> x + y;
System.out.println(add.apply(2L, 5L)); // 7

编译器根据 BinaryOperator<Long> 推断参数类型,也可以显式写出来:

BinaryOperator<Long> add = (Long x, Long y) -> x + y;

如果改成代码块,有返回值时要写 return:

BinaryOperator<Long> add = (x, y) -> {
    return x + y;
};

引用局部变量

Lambda 可以使用外层方法中的局部变量,但这个变量必须是 final,或者初始化后没有再被赋值,也就是 effectively final。

List<Integer> list = Arrays.asList(2, 5, 8, 10);
int a = 5;
Optional<Integer> optional = list.stream()
        .filter(ix -> ix > a)
        .findFirst();
System.out.println(optional.orElse(-1)); // 8

虽然 a 没有写 final,但初始化后没有改变,所以可以使用。

下面这样重新赋值,就不能通过编译:

int a = 5;
a = 100;
Runnable task = () -> System.out.println(a);

编译器会提示:Lambda 表达式引用的局部变量必须是 final 或 effectively final。

Java 8 中,匿名内部类引用局部变量也适用这个规则,并不是只有 Lambda 才能省略 final。

这里限制的是局部变量本身重新赋值,不代表它引用的对象不可修改。例如:

List<String> names = new ArrayList<>();
Runnable task = () -> names.add("Java");
task.run();
System.out.println(names); // [Java]

names 没有重新指向另一个 List,但 List 的内容发生了变化。

函数式接口

前面的 Lambda 为什么能赋给 Runnable 和 Consumer?因为它们都是函数式接口(Functional Interface),只有一个需要实现的抽象方法。默认方法和静态方法不计入,接口中与 Object 的 public 方法对应的抽象方法声明也不计入。

例如 Runnable 的主要声明:

@FunctionalInterface
public interface Runnable {
    void run();
}

@FunctionalInterface 用来做编译检查。加上它以后,如果接口不符合函数式接口的要求,编译器会报错;不加这个注解,只要接口符合要求,仍然可以使用 Lambda。

Comparator 也是函数式接口。虽然它声明了 equals(Object),但这个方法对应 Object 的 public 方法,不影响它作为 Lambda 的目标类型。

函数式接口可以有默认方法,比如 Java 8 中的 Consumer<T>:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> {
            accept(t);
            after.accept(t);
        };
    }
}

accept 是唯一的抽象方法,andThen 已经有默认实现。调用 andThen 可以把两个操作按顺序组合起来:

Consumer<String> first = text -> System.out.println("first: " + text);
Consumer<String> second = text -> System.out.println("second: " + text);
first.andThen(second).accept("hello");

输出:

first: hello
second: hello

Java 8 在 java.util.function 包里提供了不少常用的函数式接口:

接口方法用途
Consumer<T>void accept(T t)接收一个值,执行操作
Predicate<T>boolean test(T t)判断是否满足条件
Function<T, R>R apply(T t)将一个值转换成另一个值
Supplier<T>T get()不接收参数,提供一个值
BinaryOperator<T>T apply(T left, T right)接收两个同类型的值,返回同类型结果

前面 filter(ix -> ix > a) 中的 Lambda,对应的就是 Predicate<Integer>。

最后修改:2018 年 06 月 09 日
如果觉得我的文章对你有用,请随意赞赏