Java lambda表达式
lambda表达式是一个可传递的代码块,传递之后可以被执行多次。
最简单的例子是排序时传入的定制比较器Comparator:
class LenComparator implements Comparator<String>
{
public int compare(String first, String second){
return first.length() - second.length();
}
}
...
Arrays.sort(strings, new LenComparator());
或
Arrays.sort(strings, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
不能直接传递代码,Java是面向对象语言,要构造一个对象,将需要执行的方法包含在对象的方法中。
这种直接处理代码块的方式称为函数式编程。
语法
考虑这个传递代码的过程,我们实际上只需要两个要素,变量类型和表达式,而lambda表达式也是这样设计的,包含参数、箭头->以及表达式。上面的代码改写为(至于为什么能用一个lambda表达式替换comparator对象在函数式接口有解释):
Arrays.sort(strings, (String o1, String o2) -> (o1.length() - o2.length()));
如果用来计算的代码太长,用大括号
(String o1, String o2) -> {
if(o1.length() > o2.length()) return 1;
else if(o1.length() == o2.length()) return 0;
else return -1;
}
没有参数时,
() -> { for(int i = 100; i >= 0; i--) System.out.println(i); }
编译器可以推导出参数类型时可以省略,
Arrays.sort(strings, (o1, o2) -> (o1.length() - o2.length()));
如果只有一个参数且参数类型可以被推导,小括号也可以省略,
ActionListener listener = event -> System.out.println("The time is + Instant.ofEpochMilli(event.getWhen()));
lambda表达式不需要指定返回值,由编译器推导。
函数式接口
lambda表达式和封装代码的接口,比如ActionListener和Comparator,是兼容的。对于只有一个抽象方法的接口,需要这种接口时可以用一个lambda表达式替换,这种接口称为函数式接口,Comparator就是一个函数式接口。
java.util.function
包定义了很多通用函数式接口,但我现在还没有接触到,之后有机会再补充。
方法引用
基本用法
如果一个lambda表达式用了已经存在的方法,方法引用可以替代lambda表达式简化代码。
上面按照字符串长度对数组排序用方法引用可以改写为:
Arrays.sort(strings,Comparator.comparingInt(String::length));
更简单的例子是忽略大小写对字符串进行排序:
Arrays.sort(strings, String::compareToIgoreCase);
方法引用主要有三种情况:
- object::instanceMethod
- Class::instanceMethod
- Class::staticMethod
第一种:等价于传递参数的lambda表达式。比如System.out::println
,对象是System.out,实际上就是给这个对象的println方法传递一个参数,相当于lambda表达式x -> System.out.println(x)
。
第二种:我们知道,Java中方法只有两种:类的静态方法和对象的实例方法,所以在没有对象且方法不是静态方法时,需要隐式的给出一个对象,比如String::compareToIgnoreCase
相当于lambda表达式(x,y) -> x.compareToIgnore(y)
。
第三种:不需要对象,所以所有参数都会传递到静态方法,如Math::pow
等价于(x,y) -> Math.pow(x,y)
。
专门用于方法引用的方法
有些专门用于方法引用的方法,比如Objects类的isNull方法,测试obj == null比调用这个方法Objects.isNull(obj)更直观,但这样的方法可以用方法引用传递到任何有Predicate参数的方法,比如:
ArrayList<Integer> list = new ArrayList<>();
list.add(null);
list.add(1);
list.removeIf(Objects::isNull);
构造器引用
将方法引用中的方法名换成new就是构造器引用,例如要将一个字符串列表转为一个Integer对象列表,需要对每个字符串调用Integer的构造器:
ArrayList<String> strings = new ArrayList<>();
strings.add("1");
Stream<Integer> stream = strings.stream().map(Integer::new);
List<Integer> integers = stream.collect(Collectors.toList());
System.out.println(integers.get(0));
另一个类似的例子,将一个字符串列表转为一个Person对象列表,对每个字符串调用Person的构造器:
static class Person{
String name;
public Person() {}
public Person(String name) {
this.name = name;
}
}
...
ArrayList<String> names = new ArrayList<>();
names.add("charlie");
Stream<Person> stream1 = names.stream().map(Person::new);
List<Person> personList = stream1.collect(Collectors.toList());
System.out.println(personList.get(0).name);
如果有多个构造器,编译器会根据上下文推导。
变量作用域
lambda表达式可以访问外围方法或类中的参数或变量,实际上就是闭包(closure)。
public static void repeatMessage(String text, int delay)
{
ActionListener listener = event ->
{
System.out.println(text);
};
new Timer(delay, listener).start();
}
在执行这个lambda表达式时,方法repeatMessage
可能已经返回了,此时这个方法的参数也不复存在,但lambda表达式能捕获需要的变量,相当于把变量复制到一个实例对象里。但只能捕获引用值不会改变的变量,不管这个变量是在lambda表达式内部还是外部被改变,也就是初始化之后就不会再赋新值。
处理lambda表达式
使用lambda表达式的最重要的原因是可以延迟执行代码,比如触发了一定的条件再执行,如果总是可以直接执行代码那也就不需要它了。延迟执行代码的场景有很多:
- 在单独的线程中运行代码
- 多次运行代码
- 在合适的位置运行代码(排序时的比较)
- 在触发某种情况才执行代码(按钮、定时)
- …
一个简单的例子:重复打印字符串,将这个动作和重复次数传递到repeat
方法,为了接受lambda表达式,需要用函数式接口。
static void repeat(int n, Runnable action)
{
for (int i = 0; i < n; i++) {
action.run();
}
}
...
repeat(10, () -> System.out.println("hello");
Runnable
接口没有参数,也没有返回值,其抽象方法名为run
,调用这个抽象方法就会执行lambda表达式。
如果给lambda表达式传入一个表示次数的参数:
static void repeat(int n, IntConsumer action)
{
for (int i = 0; i < n; i++) {
action.accept(i);
}
}
...
repeat(10, i -> System.out.println(i + "times");
IntConsumer
接口可以处理一个整数。
还有很多其他的函数式接口用于处理不同的参数和返回值。
Comparator
Comparator
接口包含很多静态方法可以用于lambda表达式和方法引用。
comparing
方法将要比较多对象提取出一个key,对这个key进行比较。比如按名字字典顺序对Person数组进行排序。
Person[] people = new Person[]{new Person("William Re"), new Person("Alice Sun")};
Arrays.sort(people, Comparator.comparing(Person::getName));
后面还可以接thenComparing
方法,处理比较结果相同的情况。
Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
根据字符串长度排序:
Arrays.sort(people, Comparator.comparing(Person::getName), (s, t) -> Integer.compare(s.length(), t.length());