Java 8 学习笔记3——Lambda 表达式

Lambda 表达式简介

利用行为参数化来传递代码有助于应对不断变化的需求。它允许你定义一个代码块来表示一个行为,然后传递它。你可以决定在某一事件发生时(例如单击一个按钮)或在算法中的某个特定时刻(例如筛选算法中类似于“重量超过150克的苹果”的谓词,或排序中的自定义比较操作)运行该代码块。一般来说,利用这个概念,你就可以编写更为灵活且可重复使用的代码了。

但使用匿名类来表示不同的行为并不令人满意:代码十分啰嗦,这会影响程序员在实践中使用行为参数化的积极性。接下来会看到Java 8中解决这个问题的新工具——Lambda表达式。它可以让你很简洁地表示一个行为或传递代码。现在你可以把Lambda表达式看作匿名功能,它基本上就是没有声明名称的方法,但和匿名类一样,它也可以作为参数传递给一个方法。

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

  • 匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想得多!
  • 函数——我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
  • 传递——Lambda表达式可以作为参数传递给方法或存储在变量中。
  • 简洁——无需像匿名类那样写很多模板代码。

Lambda这个词是从哪儿来的?其实它来自于学术界开发出来的一套用来描述计算的λ演算法。

Java中传递代码十分繁琐和冗长,而现在,Lambda解决了这个问题:它可以让你十分简明地传递代码。理论上来说,你在Java 8之前做不了的事情,Lambda也做不了。但是,现在你用不着再用匿名类写一堆笨重的代码,来体验行为参数化的好处了!Lambda表达式鼓励你采用行为参数化风格。最终结果就是你的代码变得更清晰、更灵活。比如,利用Lambda表达式(由参数、箭头和主体组成),你可以更为简洁地自定义一个Comparator对象:

Java 8 学习笔记3——Lambda 表达式
先前:

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());

解析如下:

  • 参数列表——这里它采用了Comparatorcompare方法的参数,两个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 8 学习笔记3——Lambda 表达式
例如,在以下代码中,从一个文件中读取一行所需的模板代码(注意你使用了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并返回StringLambda。例如,下面就是从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());

四个步骤总结如下:
Java 8 学习笔记3——Lambda 表达式
上面说明了如何利用函数式接口来传递Lambda,但你还是得定义你自己的接口。