为什么要使用lambda表达式?
“lambda 表达式”是一段可以传递的代码,因此它可以被执行一次或多次。在学习语法(甚至包括一些奇怪的术语)之前,我们先回顾一下之前在Java 中一直使用的相似的代码块。
当我们要在另一个独立线程中执行一些逻辑时,通常会将代码放在一个实现Runnable 接口的类的run 方法中,如下所示:
class Worker implements Runnable {
public void run() {
for (int i = 0; i < 1000; i++)
doWork();
}
...
}
然后,当我们希望执行这段代码时,会构造一个Worker 类的实例。然后将该实例提交到一个线程池中,或者简单点,直接启动一个新的线程:
Worker w = new Worker();
new Thread(w).start();
这段代码的关键在于,run 方法中包含了你希望在另一个线程中执行的代码。我们考虑一下用一个自定义的比较器来进行排序。如果你希望按照字符串的长度而不是默认的字典顺序来排序,那么你可以将一个Comparator 对象传递给sort 方法:
class LengthComparator implements Comparator<String> {
public int compare(String first, String second) {
return Integer.compare(first.length(), second.length());
}
}
Arrays.sort(strings, new LengthComparator());
sort 方法会一直调用compare 方法,对顺序不对的元素进行重新排序,直到数组完全有序为止。你给sort 方法传递了一段需要比较元素的代码片段,而该代码会被整合到排序逻辑中,而你可能并不关心如何在那里实现。
注意:如果x=y,那么Integer.compare(x,y)会返回0;如果x<y,则它会返回一个负数;而如果x>y,则它会返回一个正数。这个静态方法已经被添加到Java 7中(请参考第9 章)。还要注意,不应该使用x-y 来比较x 和y 的大小,因为对于大的、符号相反的操作数,这种计算有可能会产生溢出。
按钮回调是另外一个会延迟执行的例子。你将回调操作放到一个实现了监听器接口的类的某个方法中,然后构造一个实例,并将实例注册到按钮上。在这种情况下,许多开发人员都会使用“匿名类的匿名实例”的方法:
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
System.out.println("Thanks for clicking!");
}
});
这里的关键是代码处于handle 方法中。该代码会在按钮被点击时执行。
注意:由于Java 8 将JavaFX 作为Swing GUI 的下一任继承者,我会在这些示例中使用JavaFX(请参考第4 章来了解更多关于JavaFX 的信息)。当然,细节并不重要,因为不管是Swing、JavaFX 还是Android,你都需要为按钮添加一些代码,以使它们在按钮被点击时可以执行。
在所有三个例子中,你会看到相同的方式。一段代码会被传递给其他调用者——也许是一个线程池、一个排序方法,或者是一个按钮。这段代码会在稍后被调用。
到现在为止,在Java 中向其他代码传递一段代码并不是很容易。你不可能将代码块到处传递。由于Java 是一个面向对象的语言,因此你不得不构建一个属于某个类的对象,由它的某个方法来包含所需的代码。
在其他一些语言中可以直接使用代码块。在很长一段时间里,Java 设计者们都拒绝加入这一特性。毕竟,Java 的一大优势在于它的简单和一致性。如果一个语言包含了所有可以略微简化代码的特性,那么它就会变得不可维护。但是,在其他那些语言中,并不只是产生线程或者注册按钮点击事件的代码变得更简单了,它们大量的API 都是更简单、更一致、更强大的。虽然我们已经通过类、对象的方式在Java 中实现了相似的API和功能,但是这些API 使用起来并不让人感到轻松和愉快。
lambda表达式的实质
lambda表达式是C++ 11的新特性。它是一个匿名函数,但是又跟函数不同。要想理解lambda表达式,特别是为什么要使用lambda表达,首先要理解回调函数。如果不太理解回调函数请戳这里。简而言之,回调函数就是被作为参数供另一个函数调用的函数(注意不是函数的返回值被另一个函数调用,而是函数的指针或者说代码被另一个函数调用)。比如void funcA(int i, bool (*funcB)(int))。 这里函数funcA有两个参数,一个是i,另一个就是函数funcB的指针。我们知道在声明一个函数时,函数参数的数据类型就确定了。所以如果要调用funcA,那么我们传递给它的参数必须是两个,一个是整型,另一个是以整型为参数以布尔型为返回值的函数类型(函数的类型由它的参数类型和返回值类型决定)。所以,我们在实现回调函数funcB时,必须按照这个参数和返回类型来实现。这就会带来一定的局限性。比如,我想让我的函数funcB更具普适性,让它多带一个参数以适应不同的情况,变成bool myFuncB(int i, int j)。那么它就不能作为参数被funcA调用了。这时lambda表达式就可以粉墨登场了。lambda表达式语法如下所示:
[capture list](parameter list)->return type{function body}
其中,参数列表和返回类型都可以省略。捕获列表也可以为空,但是[]必须写上。可见lambda表达式区别于普通函数,除了没有函数名外,还多了一个捕获列表。它之所以能解决我们之前所说的那个局限性就全靠这个捕获列表。捕获对象和参数的区别在于,捕获对象的值在程序运行到lambda表达式之前就已经确定了。而参数的值在运行到lamdba表达式之前是不确定的。举个栗子:
void fun()
{
int i=1;
int j=2;
funcA(i, [j](int k){return k>j;});
}
当程序运行funcA之前,j的值就确定了,而k的值是不确定的。k的值只有在funcA函数里面真正调用lambda表达式的时候才确定。这个程序还可以换一种更加明确的写法。
void fun()
{
int i=1;
int j=2;
function<bool(int)> f=[j](int k)(return k>j;);
funcA(i, f);
}
当程序运行到创建f对象时,j的值是确定的(它其实被作为f的一个成员变量初始化了),而k的值是不确定的。注意lambda表达式是一种特殊的数据类型(即使它和某函数具有相同参数类型和返回值类型,它们也属于不同数据类型),编译器在编译的过程中会根据我们所写的lambda表达式自动产生一个数据类型。所以声明f对象的时候我们一般只能用auto f=j(return k>j),因为我们不知道它是什么类型。但是我们之前说过回调函数的类型在声明调用者的时候(也就是声明funcA的时候)就已经确定了。所以这里有点矛盾。这个矛盾需要另一个库模版函数类型funcion来解决。有了它,只要参数和返回值类型都相同就可以定义为同一种function类型。
Lambda表达式:
Lambda是一个匿名函数,可以理解为一段可以传递的代码,将代码像数据一样进行传递,可以写出更加简介、更加灵活的代码。作为一宗更紧凑的代码风格,使Java的语言表达能力得到了提升
下面我们使用匿名内部类的方式创建一个线程
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("run ... ");
}
};
Thread thread = new Thread(runnable);
thread.start();
在上面的代码中,我们使用了Runnable接口直接创建了一个对象,用了6行代码,但是在这6行代码中,真正有用的代码只有run方法中的一行
System.out.println("run ... ");
接下来我们使用Lambda表达式来简化这些代码
Runnable runnable = ()-> System.out.println("run ...");
Thread thread = new Thread(runnable);
thread.start();
我们使用了最少的代码量,实现了同样的功能
Lambda表达式范例
需求:对教务系统中的学生进行查询操作
public class Student {
/**
* 姓名
*/
private String name;
/**
* 性别
* 男 1
* 女 0
*/
private int gender;
/**
* 年级
* 1 一年级
* 2 二年级
* 3 三年级
* 4 四年级
*/
private int grade;
}
- 需求一:获取学生中三年级及以上的学生
代码思路,将上述的学生集合作为参数,传入一个过滤的方法中,然后返回符合条件的学生集合
public List<Student> filterStudentsByGrade(List<Student> students){、
List<Student> stus = new ArrayList<>();
for (Student s : students){
if(s.getGrade() >= 3){
stus.add(s);
}
}
return stus;
}
此时,我们就完美的实现了需求中所要的功能!
但是,日常开发中程序员最痛恨的事情发生了,需求改了
- 需求二:获取学生中的男同学
思路:最简单的方式,我们写一个新的方法
public List<Student> filterStudentsByGender(List<Student> students){
List<Student> stus = new ArrayList<>();
for (Student s : students){
if(s.getGender() == 1){
stus.add(s);
}
}
return stus;
}
可是,这样的需求越来越多的时候,我们发现,代码中的方法千篇一律,每个方法中只有判断的条件不一样,其他的代码都是一样的,大量的冗余代码,要求我们必须要对代码进行优化、重构
无数的前辈们在趟过了无数的坑之后,为我们总结出来了非常好的优化代码的东西————设计模式
接下来,我们使用设计模式对上面的代码进行优化
- 创建一个简单策略接口SimpleStrategy,其中的方法operate中可以传入一个对象,然后就可以在这个接口的实现类中,进行过滤条件处理
public interface SimpleStrategy<T>{
public boolean operate(T t);
}
此时,我们看到只需要在方法中传一个SimpleStrategy的一个实现类,就能自由的进行过滤了,测试代码如下:
@Test
public void getMoreThanThreeGrade(){
List<Student> sts = filterStudentByStrategy(students, new SimpleStrategy<Student>() {
@Override
public boolean operate(Student student) {
return student.getGender() == 1;
}
});
}
虽然我们使用了策略模式,并通过匿名内部类的方式对学生信息进行了过滤,可是在整个的代码,真正有用的代码其实就一句
student.getGender() == 1;
可是我们为了完成功能,却又不得不写那些重复冗余的代码
- 福音:使用Lambda表达式,不在写重复冗余的代码
List<Student> sts = filterStudentByStrategy(students,(s)->s.getGender() == 1);
此时,我们最后的过滤代码就只有一行了,但是真的就完美了吗?未必!!!
我们在上面还创建了一个策略的接口呢,而且还声明了一个方法,代码还是很多,那么有没有什么方式,不创建接口,也不用声明一个过滤的方法,直接用一行代码就能实现上面的功能呢?
当然有,我们只需要使用Lambda表达式和Stream API就能完美的实现上述的功能了。
List<Student> sts = students.stream().filter(student -> student.getGender() == 1).collect(Collectors.toList());
这样就真正的完美了!!!