Lambda表达式是自Java SE 引入泛型以来最重大的Java语言新特性本文是年度最后一期Java Magazine中的一篇文章它介绍了Lamdba的设计初衷应用场景与基本语法
Lambda表达式这个名字由该项目的专家组选定描述了一种新的函数式编程结构这个即将出现在Java SE 中的新特性正被大家急切地等待着有时你也会听到人们使用诸如闭包函数直接量匿名函数及SAM(Single Abstract Method)这样的术语其中一些术语彼此之间会有一些细微的不同但基本上它们都指代相同的功能
虽然一开始会觉得Lambda表达式看起来很陌生但很容易就能掌握它而且为了编写可完全利用现代多核CPU的应用程序掌握Lambda表达式是至关重要的
需要牢记的一个关键概念就是Lambda表达式是一个很小且能被当作数据进行传递的函数需要掌握的第二个概念就是理解集合对象是如何在内部进行遍历的这种遍历不同于当前已有的外部顺序化遍历
在本文中我们将向你展示Lambda表达式背后的动因应用示例当然还有它的语法
为什么你需要Lambda表达式
程序员需要Lambda表达式的原因主要有三个
更紧凑的代码
通过提供额外的功能对方法的功能进行修改的能力
更好地支持多核处理
更紧凑的代码
Lambda表达式以一种简洁的方式去实现仅有一个方法的Java类
例如如果代码中有大量的匿名内部类诸如用于UI应用中的监听器与处理器实现以及用于并发应用中的Callable与Runnable实现在使用了Lambda表达式之后将使代码变得非常短且更易于理解
修改方法的能力
有时方法不具备我们想要的一些功能例如Collection接口中的contains()方法只有当传入的对象确实存在于该集合对象中时才会返回true但我们无法去干预该方法的功能比如若使用不同的大小写方案也可以认为正在查找的字符串存在于这个集合对象中我们希望此时contains()方法也能返回true
简单点儿说我们所期望做的就是将我们自己的新代码传入已有的方法中然后再调用这个传进去的代码Lambda表达式提供了一种很好的途径来代表这种被传入已有方法且应该还会被回调的代码
更好地支持多核处理
当今的CPU具备多个内核这就意味着多线程程序能够真正地被并行执行这完全不同于在单核CPU中使用时间共享这种方式通过在Java中支持函数式编程语法Lambda表达式能帮助你编写简单的代码去高效地应用这些CPU内核
例如你能够并行地操控大集合对象通过利用并行编程模式如过滤映射和化简(后面将会很快接触到这些模式)就可使用到CPU中所有可用的硬件线程
Lambda表达式概览
在前面提到的使用不同大小写方案查找字符串的例子中我们想做的就是把方法toLowerCase()的表示法作为第二个参数传入到contains()方法中为此需要做如下的工作
找到一种途径可将代码片断当作一个值(某种对象)进行处理
找到一种途径将上述代码片断传递给一个变量
换言之我们需要将一个程序逻辑包装到某个对象中并且该对象可以被进行传递为了说的更具体点儿让我们来看两个基本的Lambda表达式的例子它们都是可以被现有的Java代码进行替换的
过滤
你可能想传递的代码片断可能就是过滤器了这是一个很好的示例例如假设你正在使用(Java SE 预览版中的)javaioFileFilter去确定目录隶属于给定的路径如清单所示
清单
File dir = new File(/an/interesting/location/)
FileFilter directoryFilter = new FileFilter() {
public boolean accept(File file) {
return fileisDirectory()
}
};
File[] directories = dirlistFiles(directoryFilter)
在使用Lambda表达式之后代码会得到极大的简化如清单所示
清单
File dir = new File(/an/interesting/location/)
FileFilter directoryFilter = (File f) > fisDirectory()
File[] directories = dirlistFiles(directoryFilter)
赋值表达式的左边会推导出类型(FileFilter)右边则看起来像FileFilter接口中accept()方法的一个缩小版该方法会接受一个File对象在判定fisDirectory()之后返回一个布尔值
实际上由于Lambda表达式利用了类型推导基于后面的工作原理我们还可以进一步简化上述代码编译器知道FileFilter只有唯一的方法accept()所以它必定是该方法的实现我们还知accept()方法只需要一个File类型的参数因此f必定是File类型的如清单所示
清单
File dir = new File(/an/interesting/location/)
File[] directories = dirlistFiles(f > fisDirectory())
你可以看到使用Lambda表达式会大幅降低模板代码的数量
一旦你习惯于使用Lambda表达式它会使逻辑流程变得非常易于阅读在达到这一目的的关键方法之一就是将过滤逻辑置于使用该逻辑的方法的侧边
事件处理器
UI程序是另一个大量使用匿名内部类的领域让我们将一个点击监听器赋给一个按钮如清单所示
清单
Button button = new Button()
buttonaddActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
uishowSomething()
}
})
这多么代码无非是说当点击该按钮时调用该方法使用Lambda表达式就可写出如清单所示的代码
清单
ActionListener listener = event > {uishowSomething()};
buttonaddActionListener(listener)
该监听器在必要时可被复用但如果它仅需被使用一次清单中的代码则考虑了一种很好的方式
清单
buttonaddActionListener(event > {uishowSomething()})
在这个例子中这种使用额外花括号的语法有些古怪但这是必须的因为actionPerformed()方法返回的是void后面我们会看到与此有关的更多内容
现在让我们转而关注Lambda表达式在编写处理集合对象的新式代码中所扮演的角色尤其是当针对两种编程风格外部遍历与内部遍历之间的转换的时候
外部遍历 vs 内部遍历
到目前为止处理Java集合对象的标准方式是通过外部遍历之所以称其为外部遍历是因为要使用集合对象外部的控制流程去遍历集合所包含的元素这种传统的处理集合的方式为多数Java程序员所熟知尽管他们并不知道或不使用外部遍历这个术语
如清单所示Java语言为增强的for循环构造了一个外部迭代器并使用这个迭代器去遍历集合对象
清单
List<String> myStrings = getMyStrings()
for (String myString : myStrings) {
if (ntains(possible))
Systemoutprintln(myString + contains + possible)
}
}
使用这种方法集合类代表着全部元素的一个整体视图并且该集合对象还能支持对任意元素的随机访问程序员可能会有这种需求
基于这种观点可通过调用iterator()方法去遍历集合对象该方法将返回集合元素类型的迭代器该迭代器是针对同一集合对象的更具限制性的视图它没有为随机访问暴露任何接口相反它纯粹是为了顺序地访问集合元素而设计的这种顺序本性使得当你试图并发地访问集合对象时就会造成臭名昭着的ConcurrentModificationException
另一种可选的方案就是要求集合对象要能够在内部管理迭代器(或循环)这种方案就是内部遍历当使用Lambda表达式时会优先选择内部遍历
除了新的Lambda表达式语法以外Lambda项目还包括一个经过大幅升级的集合框架类库这次升级的目的是为了能更易于编写使用内部遍历的代码以支持一系列众所周知的函数式编程典范
使用Lambda的函数式编程
曾经大多数开发者发现他们需要集合能够执行如下一种或几种操作
创建一个新的集合对象但要过滤掉不符合条件的元素
对集合中的元素逐一进行转化并使用转化后的集合
创建集合中所有元素的某个属性的总体值例如合计值与平均值这样的任务(分别称之为过滤映射和化简)具有共通的要点它们都需要处理集合中的每个元素
程序无论是判定某个元素是否存在或是判断元素是否符合某个条件(过滤)或是将元素转化成新元素并生成新集合(映射)或是计算总体值(化简)关键原理就是程序必须处理到集合中的每个元素
这就暗示我们需要一种简单的途径去表示用于内部遍历的程序幸运地是Java SE 为此类表示法提供了构建语句块
支持基本函数式编程的Java SE 类
Java SE 中的一些类意在被用于实现前述的函数式典范这些类包括PredicateMapper和Block当然还有其它的一些类它们都在一个新的javautilfunctions包中
看看Predicate类的更多细节该类常被用于实现过滤算法将它作用于一个集合以返回一个包含有符合谓语条件元素的新集合何为谓语有很多种解释Java SE 认为谓语是一个依据其变量的值来判定真或假的方法
再考虑一下我们之前看过的一个例子给定一个字符串的集合我们想判定它是否包含有指定的字符串但希望字符串的比较是大小写不敏感的
在Java SE 中我们将需要使用外部遍历其代码将如清单所示
清单
public void printMatchedStrings(List<String> myStrings) {
List<String> out = new ArrayList<>()
for (String s: myStrings) {
if (sequalsIgnoreCase(possible))
outadd(s)
}
log(out)
}
而在即将发布的Java SE 中我们使用Predicate以及Collections类中一个新的助手方法(过滤器)就可写出更为紧凑的程序如清单所示
清单
public void printMatchedStrings() {
Predicate<String> matched = s > sequalsIgnoreCase(possible)
log(myStringsfilter(matched))
}
事实上如果使用更为通用的函数式编程风格你只需要写一行代码如清单所示
清单
public void printMatchedStrings() {
log(myStringsfilter(s > sequalsIgnoreCase(possible)))
}
如你所见代码依然非常的易读并且我们也体会到了使用内部遍历的好处
最后让我们讨论一下Lambda表达式语法的更多细节
Lambda表达式的语法规则
Lambda表达式的基本格式是以一个可被接受的参数列表开头以一些代码(称之为表达式体/body)结尾并以箭头(>)将前两者分隔开
注意Lambda表达式的语法仍可能会面临改变但在撰写本文的时候下面示例中所展示的语法是能够正常工作的
Lambda表达式非常倚重类型推导与Java的其它语法相比这显得极其不同寻常
让我们进一步考虑之前已经看过的一个示例(请见清单)如果看看ActionListener的定义可以发现它只有一个方法(请见清单)
清单
ActionListener listener = event > {uishowSomething()};
清单
public interface ActionListener {
public void actionPerformed(ActionEvent event)
}
所以在清单右侧的Lambda表达式能够很容易地理解为这是针对仅声明单个方法的接口的方法定义注意仍然必须要遵守Java静态类型的一般规则这是使类型推导能正确工作的唯一途径
据此可以发现使用Lambda表达式可以将先前所写的匿名内部类代码转换更紧凑的代码
还需要意识到有另一个怪异的语法让我们再回顾下上述示例如清单所示
清单
FileFilter directoryFilter = (File f) > fisDirectory()
仅一瞥之它看起来与ActionListener的示例相似但让我们看看FileFilter接口的定义(请见清单)accept()方法会返回一个布尔值但并没有一个显式的返回语句相反该返回值的类型是从Lambda表达式中推导出来的
清单
public interface FileFilter {
public boolean accept(File pathname)
}
这就能解释当方法返回类型为void时为什么要进行特别处理了对于这种情形Lambda表达式会使用一对额外的小括号去包住代码部分(表达式体/body)若没有这种怪异的语法类型推导将无法正常工作但你要明白这一语法可能会被改变
Lambda表达式的表达式体可以包含多条语句对于这种情形表达式体需要被小括号包围住但被推导出的返回类型这种语法将不启作用那么返回类型关键字就必不可少
最后还需要提醒你的是当前IDE似乎还不支持Lambda语法所以当你第一次尝试Lambda表达式时必须要格外注意javac编译器抛出的任何警告
结论
Lambda表达式是自Java SE 引入泛型以来最重大的Java语言新特性应用得当Lambda表达式可使你写出简洁的代码为已有方法增加额外的功能并能更好地适应多核处理器到目前为止我们能肯定的是你正急切地想去尝试Lambda表达式所以咱也别啰嗦了…
你可以从Lambda项目的主页中获得包含有Lambda表达式的Java SE 快照版同样地在试用二进制包时你也应该先阅读一下Lambda项目状态的相关文章可以在此处找到它们