Lambda表达式是自Java SE 5引入泛型以来最重大的Java语言新特性,本文是2012年度最后一期
Java Magazine中的
一篇文章,它介绍了Lamdba的设计初衷,应用场景与基本语法。(2013.01.04最后更新)
Lambda表达式,这个名字由该项目的专家组选定,描述了一种新的函数式编程结构,这个即将出现在Java SE 8中的新特性正被大家急切地等待着。有时你也会听到人们使用诸如闭包,函数直接量,匿名函数,及SAM(Single Abstract Method)这样的术语。其中一些术语彼此之间会有一些细微的不同,但基本上它们都指代相同的功能。
虽然一开始会觉得Lambda表达式看起来很陌生,但很容易就能掌握它。而且为了编写可完全利用现代多核CPU的应用程序,掌握Lambda表达式是至关重要的。
需要牢记的一个关键概念就是,Lambda表达式是一个很小且能被当作数据进行传递的函数。需要掌握的第二个概念就是,理解集合对象是如何在内部进行遍历的,这种遍历不同于当前已有的外部顺序化遍历。
在本文中,我们将向你展示Lambda表达式背后的动因,应用示例,当然,还有它的语法。
为什么你需要Lambda表达式
程序员需要Lambda表达式的原因主要有三个:
1. 更紧凑的代码
2. 通过提供额外的功能对方法的功能进行修改的能力
3. 更好地支持多核处理
更紧凑的代码
Lambda表达式以一种简洁的方式去实现仅有一个方法的Java类。
例如,如果代码中有大量的匿名内部类--诸如用于UI应用中的监听器与处理器实现,以及用于并发应用中的Callable与Runnable实现--在使用了Lambda表达式之后,将使代码变得非常短,且更易于理解。
修改方法的能力
有时,方法不具备我们想要的一些功能。例如,Collection接口中的contains()方法只有当传入的对象确实存在于该集合对象中时才会返回true。但我们无法去干预该方法的功能,比如,若使用不同的大小写方案也可以认为正在查找的字符串存在于这个集合对象中,我们希望此时contains()方法也能返回true。
简单点儿说,我们所期望做的就是"将我们自己的新代码传入"已有的方法中,然后再调用这个传进去的代码。Lambda表达式提供了一种很好的途径来代表这种被传入已有方法且应该还会被回调的代码。
更好地支持多核处理
当今的CPU具备多个内核。这就意味着,多线程程序能够真正地被并行执行,这完全不同于在单核CPU中使用时间共享这种方式。通过在Java中支持函数式编程语法,Lambda表达式能帮助你编写简单的代码去高效地应用这些CPU内核。
例如,你能够并行地操控大集合对象,通过利用并行编程模式,如过滤、映射和化简(后面将会很快接触到这些模式),就可使用到CPU中所有可用的硬件线程。
Lambda表达式概览
在前面提到的使用不同大小写方案查找字符串的例子中,我们想做的就是把方法toLowerCase()的表示法作为第二个参数传入到contains()方法中,为此需要做如下的工作:
1. 找到一种途径,可将代码片断当作一个值(某种对象)进行处理
2. 找到一种途径,将上述代码片断传递给一个变量
换言之,我们需要将一个程序逻辑包装到某个对象中,并且该对象可以被进行传递。为了说的更具体点儿,让我们来看两个基本的Lambda表达式的例子,它们都是可以被现有的Java代码进行替换的。
过滤
你想传递的代码片断可能就是过滤器,这是一个很好的示例。例如,假设你正在使用(Java SE 7预览版中的)java.io.FileFilter去确定目录隶属于给定的路径,如清单1所示,
清单1 File dir = new File("/an/interesting/location/");
FileFilter directoryFilter = new FileFilter() {
public boolean accept(File file) {
return file.isDirectory();
}
};
File[] directories = dir.listFiles(directoryFilter);
在使用Lambda表达式之后,代码会得到极大的简化,如清单2所示,
清单2
File dir = new File("/an/interesting/location/");
FileFilter directoryFilter = (File f) -> f.isDirectory();
File[] directories = dir.listFiles(directoryFilter);
赋值表达式的左边会推导出类型(FileFilter),右边则看起来像FileFilter接口中accept()方法的一个缩小版,该方法会接受一个File对象,在判定f.isDirectory()之后返回一个布尔值。
实际上,由于Lambda表达式利用了类型推导,基于后面的工作原理,我们还可以进一步简化上述代码。编译器知道FileFilter只有唯一的方法accept(),所以它必定是该方法的实现。我们还知,accept()方法只需要一个File类型的参数。因此,f必定是File类型的。如清单3所示,
清单3
File dir = new File("/an/interesting/location/");
File[] directories = dir.listFiles(f -> f.isDirectory());
你可以看到,使用Lambda表达式会大幅降低模板代码的数量。
一旦你习惯于使用Lambda表达式,它会使逻辑流程变得非常易于阅读。在达到这一目的的关键方法之一就是将过滤逻辑置于使用该逻辑的方法的侧边。
事件处理器
UI程序是另一个大量使用匿名内部类的领域。让我们将一个点击监听器赋给一个按钮,如清单4所示,
清单4
Button button = new Button();
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ui.showSomething();
}
});
这多么代码无非是说"当点击该按钮时,调用该方法"。使用Lambda表达式就可写出如清单5所示的代码,
清单5
ActionListener listener = event -> {ui.showSomething();};
button.addActionListener(listener);
该监听器在必要时可被复用,但如果它仅需被使用一次,清单6中的代码则考虑了一种很好的方式。
清单6
button.addActionListener(event -> {ui.showSomething();});
在这个例子中,这种使用额外花括号的语法有些古怪,但这是必须的,因为actionPerformed()方法返回的是void。后面我们会看到与此有关的更多内容。
现在让我们转而关注Lambda表达式在编写处理集合对象的新式代码中所扮演的角色,尤其是当针对两种编程风格,外部遍历与内部遍历,之间的转换的时候。
外部遍历 vs. 内部遍历
到目前为止,处理Java集合对象的标准方式是通过外部遍历。之所以称其为外部遍历,是因为要使用集合对象外部的控制流程去遍历集合所包含的元素。这种传统的处理集合的方式为多数Java程序员所熟知,尽管他们并不知道或不使用外部遍历这个术语。
如清单7所示,Java语言为增强的for循环构造了一个外部迭代器,并使用这个迭代器去遍历集合对象,
清单7
List<String> myStrings = getMyStrings();
for (String myString : myStrings) {
if (myString.contains(possible))
System.out.println(myString + " contains " + possible);
}