为何要引入Lambda表达式
对于一个Java变量,我们可以对其赋值,但如果我们想对一个代码块赋值应该怎么做?
在Java 8 之前,这是无法做到的,Lambda表达式的出现,使代码块可以赋值。但上面的写法并不是最简洁的,实际使用中,可以移除一些无用的声明。
这样,就把一段代码赋值给一个变量。由于java是强类型语言,这里还有一个问题,这个变量的类型是什么?==在java中,所有Lambda表达式类型都是一个接口,而Lambda表达式本身,就是那个接口的实现。==上面的Lambda表达式加上类型后如下所示:
这种只有一个函数需要实现的接口类型(只有一个抽象函数,仍可包含静态函数和默认函数),成为“函数式接口”。为了避免别人在接口中增加函数,可以在上面加上一个声明@FunctionalInterface。
这样,就得到如下完整的Lambda表达式声明:
MyLambdaInterface aBlockOfCode = (s) -> System.out.println(s);
java 8在java.util.function包中预定义了大量函数式接口,典型的包含如下4类接口:
- XxxFunction:这类接口中通常包含一个apply()抽象方法,该方法对参数进行处理、转换,然后返回一个新的值。通常用于对指定数据进行转换处理。
- XxxConsumer:这类接口中通常包含一个accept()抽象方法,与apply()方法基本相似,也负责对参数进行处理,不过不返回处理结果。
- XxxPredicate:这类接口通常包含一个test()抽象方法,通常用来对参数进行判断,然后返回一个boolean值。该接口通常用于判断参数是否满足指定条件,经常用于进行筛选过滤数据。
- XxxSupplier:这类接口通常包含一个getAsXxx()的抽象方法,该方法不需要输入参数,会按某种逻辑算法返回一个数据。
如何使用Lambda表达式
使用Lambda表达式的重点是延迟执行。如果想要立即执行代码,完全可以直接执行,无需将它包装在一个Lambda表达式中。之所以希望以后再执行,可能有以下原因:
- 在一个单独的线程中执行代码;
- 多次运行代码;
- 在算法的适当位置运行代码(如 排序的比较操作);
- 发生某种情况时执行代码(如 点击按钮等);
- 只在必要时才运行代码。
lambda是一个可传递的代码块,可以在以后执行一次或多次。我们先观察Java中哪些地方可能用过这种代码块。
如果想按指定时间间隔完成工作,可以把工作放在ActionListener的ActionPerformed方法中:
class Worker implements ActionListener{
public void actionPerformed(ActionEvent event){
...
do something
}
}
想要反复执行上述代码,可以构造Worker的实例。然后把该实例对象提交到一个Timer对象。重点是actionPerformed方法中包含希望以后执行的代码。
或者考虑定义比较器进行排序。如果想按长度而不是默认的字典顺序对字符串排序,可以向sort方法中传入Comparator对象:
class LengthComparator implements Comparator<String>{
public int compare(String first, String second){
return first.length() - second.length();
}
}
...
Arrays.sort(stringArray,new LengthComparator());
上述两个例子都有一些共同点,都是将一个代码块传递到某个对象(一个定时器/sort方法)。这个代码块将在某个时间调用。Java是面向对象语言,在java 8以前,传递这样的代码块并不容易,必须构造一个对象,该对象所属的类需要有一个方法能包含所需的代码。
有了Lambda表达式,可直接给出方法的实现,不需要新建对象繁琐的操作。不过lambda表达式要求目标类型必须是函数式接口,只能声明一个抽象方法。为保证lambda表达式的目标类型是一个明确的函数式接口,可以有如下三种常见方式:
- 将Lambda表达式赋值给函数式接口的变量
- 将lambda表达式作为函数式接口类型的参数传递给某个方法
- 使用函数式接口对lambda表达式进行强制类型转换(这样才可赋值给Object对象,否则只能赋值给函数式接口变量)
方法引用与构造器引用
可能使用现有的方法可以完成传递到代码中的动作。如果lambda表达式的代码块只有一条语句,程序可以省略代码块的花括号。不仅如此,如果只有一条代码,还可以在代码块中使用方法引用和构造器引用。
种类 | 示例 | 说明 | 对应的lambda表达式 |
---|---|---|---|
引用类方法 | 类名::类方法 | 接口中被实现方法的全部参数传给该类方法作为参数 | (a,b…)->类名.类方法(a,b…) |
引用特定对象的实例方法 | 特定对象::实例方法 | 接口中被实现方法的全部参数传给该类方法作为参数 | (a,b…)->特定对象.实例方法(a,b…) |
引用某类对象的实例方法 | 类名::实例方法 | 接口中实现的方法第一个参数作为调用者,后面参数传递给该方法作为参数 | (a,b…)->a.实例方法(b,…) |
引用构造器 | 类名::new | 接口中被实现方法的全部参数传给该类方法作为参数 | (a,b…)->new 类名(a,b…) |
假设希望出现一个定时器事件就打印这个事件对象。为此可以这样调用:
Timer t = new Timer(1000,event->System.out.println(event));
由于借用已有的方法实现传递到代码的动作,如果直接把pringln方法传递到Timer构造器就更好了,传递的参数直接作为该方法的参数。具体做法如下:
Timer t = new Timer(1000,System.out::println);
表达式System.out::println是一个方法引用,它等价于Lambda表达式x -> System.out.println(x)。
再看一个例子,假设想要对字符串排序,而不考虑字符的大小写。可以传递一下方法表达式:
Arrays.sort(strings, String::compareToIgnoreCase)
从这些例子可看出,要使用::操作符分割方法名/对象名。主要有3种情况:
- object::instanceMethod
- Class::staticMethod
- Class::instanceMethod
前两种情况,方法的引用等价于提供方法参数的Lambda表达式。如前所示,System.out.println等价于x -> System.out.println(x)。类似的,Math::pow等价于(x,y) -> Math.pow(x,y)。
对于第三种情况,第一个参数会成为方法引用的目标。如String::compareToIgnoreCase等价于(x,y) -> x.compareToIgnoreCase(y)。
构造器引用和数组引用很类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用,具体是哪个构造器取决于上下文和传入的参数。假设有一个人名的字符串列表,想把它转换为一个Person对象数组,需要在各个字符串上调用构造器,调用如下:
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collections.toList());
map方法会为各个列表元素调用Person(String)的构造器。如果有多个构造器,编译器会从上下文中选择String类型参数的构造器。