Lambda 表达式简介
利用行为参数化来传递代码有助于应对不断变化的需求。它允许你定义一个代码块来表示一个行为,然后传递它。你可以决定在某一事件发生时(例如单击一个按钮)或在算法中的某个特定时刻(例如筛选算法中类似于“重量超过150克的苹果”的谓词,或排序中的自定义比较操作)运行该代码块。一般来说,利用这个概念,你就可以编写更为灵活且可重复使用的代码了。
但使用匿名类来表示不同的行为并不令人满意:代码十分啰嗦,这会影响程序员在实践中使用行为参数化的积极性。接下来会看到Java 8中解决这个问题的新工具——Lambda表达式。它可以让你很简洁地表示一个行为或传递代码。现在你可以把Lambda表达式看作匿名功能,它基本上就是没有声明名称的方法,但和匿名类一样,它也可以作为参数传递给一个方法。
可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
- 匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想得多!
-
函数——我们说它是函数,是因为
Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。 -
传递——
Lambda表达式可以作为参数传递给方法或存储在变量中。 - 简洁——无需像匿名类那样写很多模板代码。
Lambda这个词是从哪儿来的?其实它来自于学术界开发出来的一套用来描述计算的λ演算法。
在Java中传递代码十分繁琐和冗长,而现在,Lambda解决了这个问题:它可以让你十分简明地传递代码。理论上来说,你在Java 8之前做不了的事情,Lambda也做不了。但是,现在你用不着再用匿名类写一堆笨重的代码,来体验行为参数化的好处了!Lambda表达式鼓励你采用行为参数化风格。最终结果就是你的代码变得更清晰、更灵活。比如,利用Lambda表达式(由参数、箭头和主体组成),你可以更为简洁地自定义一个Comparator对象:
先前:
Comparator<Apple> byWeight=new Comparator<Apple>() {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
};
现在(用了Lambda表达式):
Comparator<Apple> byWeight=(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
解析如下:
-
参数列表——这里它采用了
Comparator中compare方法的参数,两个Apple -
箭头——箭头
->把参数列表与Lambda主体分隔开。 -
Lambda主体——比较两个
Apple的重量。表达式就是Lambda的返回值了。
下面是Java 8中五个有效的Lambda表达式的例子:
//1. 具有一个String类型的参数并返回一个int,Lambda没有return语句,因为已经隐含了return
(String s) -> s.length()
//2. 有一个Apple类型的参数并返回一个boolean(苹果的重量是否超过150克)
(Apple a) -> a.getWeight() > 150
//3. 具有两个int类型的参数而没有返回值(void返回),注意Lambda表达式可以包含多行语句,这里是两行
(int x,int y) -> (
System.out.println("Result:");
System.out.println(x+y);
)
//4. 没有参数,返回一个int
() -> 42
//5. 具有两个Apple类型的参数,返回一个int;比较两个Apple的重量
(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
Lambda 表达式基本语法和例子
Lambda的基本语法如下:
(parameters) -> expression
或(注意语句的花括号)
(parameters) -> { statements; }
下表是一些Lambda的例子和使用案例:
| 使用案例 | Lambda示例 |
|---|---|
| 布尔表达式 | (List<String> list) -> list.isEmpty() |
| 创建对象 | () -> new Apple(10) |
| 消费一个对象 | (Apple a) -> { System.out.println(a.getWeight(); } |
| 从一个对象中选择/抽取 | (String s) -> s.length() |
| 组合两个值 | (int a,int b) -> a*b |
| 比较两个对象 | (Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) |
在哪里以及如何使用Lambda 表达式
函数式接口
函数式接口就是只定义一个抽象方法的接口。注意,接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。
用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。
下面是一个在函数式接口(Runnable)使用Lambda 表达式的例子:
//java.lang.Runnable
public interface Runnable{
public void run();
}
Runnable r1=() -> System.out.println("Hello World 1"); //使用Lambda
Runnable r2=new Runnable(){ //使用匿名类
public void run(){
System.out.println("Hello World 2");
}
};
public static void process(Runnable r){
r.run();
}
process(r1); //打印“Hello World 1”
process(r2); //打印“Hello World 2”
process(() -> System.out.println("Hello World 3")); //利用直接传递的Lambda打印“Hello World 3”
函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。
我们可以使用一个特殊表示法来描述Lambda和函数式接口的签名。() -> void代表了参数列表为空,且返回void的函数。这正是Runnable接口所代表的。举另一个例子,(Apple,Apple) -> int代表接受两个Apple作为参数且返回int的函数。
Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法,当然这个Lambda表达式的签名要和函数式接口的抽象方法一样。比如,在我们之前的例子里,你可以像下面这样直接把一个Lambda传给process方法:
public void process(Runnable r){
r.run();
}
process(() -> System.out.println("This is awesome!!"));
此代码执行时将打印“This is awesome!!”。Lambda表达式() -> System.out.println("This is awesome!!")不接受参数且返回void。这恰恰是Runnable接口中run方法的签名。
如果你去看看新的Java API,会发现函数式接口带有@FunctionalInterface的标注。这个标注用于表示该接口会设计成一个函数式接口。
如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override标注表示方法被重写了。
把Lambda付诸实践:环绕执行模式
资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。这就是所谓的环绕执行(execute around)模式,如下图所示,任务A和任务B周围都环绕着进行准备/清理的同一段冗余代码。
例如,在以下代码中,从一个文件中读取一行所需的模板代码(注意你使用了Java 7中的带资源的try语句,它已经简化了代码,因为你不需要显式地关闭资源了):
public static String processFile() throws IOException{
try(BufferedReader br=new BufferedReader(new FileReader("data.txt"))){
returnbr.readLine(); //这就是做有用工作的那行代码
}
}
第1步,行为参数化
现在上面这段代码是有局限的。你只能读文件的第一行。如果你想要返回头两行,甚至是返回使用最频繁的词,该怎么办呢?在理想的情况下,你要重用执行设置和清理的代码,并告诉processFile方法对文件执行不同的操作。那么,你需要把processFile的行为参数化。你需要一种方法把行为传递给processFile,以便它可以利用BufferedReader执行不同的行为。
传递行为正是Lambda的拿手好戏。如果想一次读两行,在这个新的processFile方法中,基本上,你需要一个接收BufferedReader并返回String的Lambda。例如,下面就是从BufferedReader中打印两行的写法:
String result=processFile((BufferedReader br) -> br.readLine()+br.readLine());
第2步,使用函数式接口来传递行为
Lambda仅可用于上下文是函数式接口的情况。你需要创建一个能匹配BufferedReader->String,还可以抛出IOException异常的接口:
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
现在你就可以把这个接口作为新的processFile方法的参数了:
public static String processFile(BufferedReaderProcessor p) throws IOException{
…
}
第3步,执行一个行为
任何BufferedReader->String形式的Lambda都可以作为参数来传递,因为它们符合BufferedReaderProcessor接口中定义的process方法的签名。现在你只需要一种方法在processFile主体内执行Lambda所代表的代码。请记住,Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。因此,你可以在processFile主体内,对得到的BufferedReaderProcessor对象调用process方法执行处理:
public static String processFile(BufferedReaderProcessor p) throws IOException{
try(BufferedReader br=new BufferedReader(newFileReader("data.txt"))){
return p.process(br); //处理BufferedReader对象
}
}
第4步,传递Lambda
现在你就可以通过传递不同的Lambda重用processFile方法,并以不同的方式处理文件了。
处理一行:
String oneLine=processFile((BufferedReader br) -> br.readLine());
处理两行:
String twoLines=processFile((BufferedReader br) -> br.readLine()+br.readLine());
四个步骤总结如下:
上面说明了如何利用函数式接口来传递Lambda,但你还是得定义你自己的接口。