在 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: helloJava 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>。