Java 8 学习笔记3——Lambda 表达式
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
,但你还是得定义你自己的接口。