探索Java语言与JVM中的Lambda表达式
Lambda表达式是自Java SE 5引入泛型以来最重大的Java语言新特性,本文是2012年度最后一期Java Magazine中的一篇文章,它介绍了Lamdba的设计初衷,应用场景与基本语法。(2013.01.02最后更新)
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
1
2
3
4
5
6
7
|
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
1
2
3
|
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
1
2
|
File dir = new File( "/an/interesting/location/" ); File[] directories = dir.listFiles(f -> f.isDirectory()); |
你可以看到,使用Lambda表达式会大幅降低模板代码的数量。
一旦你习惯于使用Lambda表达式,它会使逻辑流程变得非常易于阅读。在达到这一目的的关键方法之一就是将过滤逻辑置于使用该逻辑的方法的侧边。
事件处理器
UI程序是另一个大量使用匿名内部类的领域。让我们将一个点击监听器赋给一个按钮,如清单4所示,
清单4
1
2
3
4
5
6
|
Button button = new Button(); button.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { ui.showSomething(); } }); |
这多么代码无非是说”当点击该按钮时,调用该方法”。使用Lambda表达式就可写出如清单5所示的代码,
清单5
1
2
|
ActionListener listener = event -> {ui.showSomething();}; button.addActionListener(listener); |
该监听器在必要时可被复用,但如果它仅需被使用一次,清单6中的代码则考虑了一种很好的方式。
清单6
1
|
button.addActionListener(event -> {ui.showSomething();}); |
在这个例子中,这种使用额外花括号的语法有些古怪,但这是必须的,因为actionPerformed()方法返回的是void。后面我们会看到与此有关的更多内容。
现在让我们转而关注Lambda表达式在编写处理集合对象的新式代码中所扮演的角色,尤其是当针对两种编程风格,外部遍历与内部遍历,之间的转换的时候。
外部遍历 vs. 内部遍历
到目前为止,处理Java集合对象的标准方式是通过外部遍历。之所以称其为外部遍历,是因为要使用集合对象外部的控制流程去遍历集合所包含的元素。这种传统的处理集合的方式为多数Java程序员所熟知,尽管他们并不知道或不使用外部遍历这个术语。
如清单7所示,Java语言为增强的for循环构造了一个外部迭代器,并使用这个迭代器去遍历集合对象,
清单7
1
2
3
4
5
6
|
List<String> myStrings = getMyStrings(); for (String myString : myStrings) { if (myString.contains(possible)){ System.out.println(myString + " contains " + possible); } } |
使用这种方法,集合类代表着全部元素的一个”整体”视图,并且该集合对象还能支持对任意元素的随机访问,程序员可能会有这种需求。
基于这种观点,可通过调用iterator()方法去遍历集合对象,该方法将返回集合元素类型的迭代器,该迭代器是针对同一集合对象的更具限制性的视图。它没有为随机访问暴露任何接口;相反,它纯粹是为了顺序地访问集合元素而设计的。这种顺序本性使得当你试图并发地访问集合对象时就会造成臭名昭著的ConcurrentModificationException。
另一种可选的方案就是要求集合对象要能够在内部管理迭代器(或循环),这种方案就是内部遍历,当使用Lambda表达式时会优先选择内部遍历。
除了新的Lambda表达式语法以外,Lambda项目还包括一个经过大幅升级的集合框架类库。这次升级的目的是为了能更易于编写使用内部遍历的代码,以支持一系列众所周知的函数式编程典范。
使用Lambda的函数式编程
曾经,大多数开发者发现他们需要集合能够执行如下一种或几种操作:
1. 创建一个新的集合对象,但要过滤掉不符合条件的元素。
2. 对集合中的元素逐一进行转化,并使用转化后的集合。
3. 创建集合中所有元素的某个属性的总体值,例如,合计值与平均值。这样的任务(分别称之为过滤,映射和化简)具有共通的要点:它们都需要处理集合中的每个元素。
程序无论是判定某个元素是否存在,或是判断元素是否符合某个条件(过滤),或是将元素转化成新元素并生成新集合(映射),或是计算总体值(化简),关键原理就是”程序必须处理到集合中的每个元素”。
这就暗示我们需要一种简单的途径去表示用于内部遍历的程序。幸运地是,Java SE 8为此类表示法提供了构建语句块。
支持基本函数式编程的Java SE 8类
Java SE 8中的一些类意在被用于实现前述的函数式典范,这些类包括Predicate,Mapper和Block–当然,还有其它的一些类–它们都在一个新的java.util.functions包中。
看看Predicate类的更多细节,该类常被用于实现过滤算法;将它作用于一个集合,以返回一个包含有符合谓语条件元素的新集合。何为谓语,有很多种解释。Java SE 8认为谓语是一个依据其变量的值来判定真或假的方法。
再考虑一下我们之前看过的一个例子。给定一个字符串的集合,我们想判定它是否包含有指定的字符串,但希望字符串的比较是大小写不敏感的。
在Java SE 7中,我们将需要使用外部遍历,其代码将如清单8所示,
清单8
1
2
3
4
5
6
7
8
|
public void printMatchedStrings(List<String> myStrings) { List<String> out = new ArrayList<>(); for (String s: myStrings) { if (s.equalsIgnoreCase(possible)) out.add(s); } log(out); } |
而在即将发布的Java SE 8中,我们使用Predicate以及Collections类中一个新的助手方法(过滤器)就可写出更为紧凑的程序,如清单9所示,
清单9
1
2
3
4
|
public void printMatchedStrings() { Predicate<String> matched = s -> s.equalsIgnoreCase(possible); log(myStrings.filter(matched)); } |
事实上,如果使用更为通用的函数式编程风格,你只需要写一行代码,如清单10所示,
清单10
1
2
3
|
public void printMatchedStrings() { log(myStrings.filter(s -> s.equalsIgnoreCase(possible))); } |
如你所见,代码依然非常的易读,并且我们也体会到了使用内部遍历的好处。
最后,让我们讨论一下Lambda表达式语法的更多细节。
Lambda表达式的语法规则
Lambda表达式的基本格式是以一个可被接受的参数列表开头,以一些代码(称之为表达式体/body)结尾,并以箭头(->)将前两者分隔开。
注意:Lambda表达式的语法仍可能会面临改变,但在撰写本文的时候,下面示例中所展示的语法是能够正常工作的。
Lambda表达式非常倚重类型推导,与Java的其它语法相比,这显得极其不同寻常。
让我们进一步考虑之前已经看过的一个示例(请见清单11)。如果看看ActionListener的定义,可以发现它只有一个方法(请见清单12)。
清单11
1
|
ActionListener listener = event -> {ui.showSomething();}; |
清单12
1
2
3
|
public interface ActionListener { public void actionPerformed(ActionEvent event); } |
所以,在清单11右侧的Lambda表达式,能够很容易地理解为”这是针对仅声明单个方法的接口的方法定义”。注意,仍然必须要遵守Java静态类型的一般规则;这是使类型推导能正确工作的唯一途径。
据此可以发现,使用Lambda表达式可以将先前所写的匿名内部类代码转换更紧凑的代码。
还需要意识到有另一个怪异的语法。让我们再回顾下上述示例,如清单13所示,
清单13
1
|
FileFilter directoryFilter = (File f) -> f.isDirectory(); |
仅一瞥之,它看起来与ActionListener的示例相似,但让我们看看FileFilter接口的定义(请见清单14)。accept()方法会返回一个布尔值,但并没有一个显式的返回语句。相反,该返回值的类型是从Lambda表达式中推导出来的
清单14
1
2
3
|
public interface FileFilter { public boolean accept(File pathname); } |
这就能解释,当方法返回类型为void时,为什么要进行特别处理了。对于这种情形,Lambda表达式会使用一对额外的小括号去包住代码部分(表达式体/body)。若没有这种怪异的语法,类型推导将无法正常工作–但你要明白,这一语法可能会被改变。
Lambda表达式的表达式体可以包含多条语句,对于这种情形,表达式体需要被小括号包围住,但”被推导出的返回类型”这种语法将不启作用,那么返回类型关键字就必不可少。
最后还需要提醒你的是:当前,IDE似乎还不支持Lambda语法,所以当你第一次尝试Lambda表达式时,必须要格外注意javac编译器抛出的任何警告。
结论
Lambda表达式是自Java SE 5引入泛型以来最重大的Java语言新特性。应用得当,Lambda表达式可使你写出简洁的代码,为已有方法增加额外的功能,并能更好地适应多核处理器。到目前为止,我们能肯定的是,你正急切地想去尝试Lambda表达式,所以咱也别啰嗦了…
你可以从Lambda项目的主页中获得包含有Lambda表达式的Java SE 8快照版。同样地,在试用二进制包时,你也应该先阅读一下”Lambda项目状态”的相关文章,可以在此处找到它们。
探索Java语言与JVM中的Lambda表达式的更多相关文章
- Java语言与JVM中的Lambda表达式全解
Lambda表达式是自Java SE 5引入泛型以来最重大的Java语言新特性,本文是2012年度最后一期Java Magazine中的一篇文章,它介绍了Lamdba的设计初衷,应用场景与基本语法. ...
- Lambda 表达式,Java中应用Lambda 表达式
一.Lambda 表达式 简单来说,编程中提到的 lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数. 链接:知乎 先举一个普通的 Python 例 ...
- Java对象在JVM中的生命周期
当你通过new语句创建一个java对象时,JVM就会为这个对象分配一块内存空间,只要这个对象被引用变量引用了,那么这个对象就会一直驻留在内存中,否则,它就会结束生命周期,JVM会在合适的时 ...
- C#中的Lambda表达式和表达式树
在C# 2.0中,通过方法组转换和匿名方法,使委托的实现得到了极大的简化.但是,匿名方法仍然有些臃肿,而且当代码中充满了匿名方法的时候,可读性可能就会受到影响.C# 3.0中出现的Lambda表达式在 ...
- JDK1.8中的Lambda表达式和Stream
1.lambda表达式 Java8最值得学习的特性就是Lambda表达式和Stream API,如果有python或者javascript的语言基础,对理解Lambda表达式有很大帮助,因为Java正 ...
- Android中使用Lambda表达式开发
参考文章:ImportNew 要在Android开发中使用lambda表达式,首先需要在 Module 的build.gradle中加入: compileOptions { targetCompati ...
- Android中使用lambda表达式
lambda 语法简介 视频为本篇播客知识点讲解,建议采用超清模式观看, 欢迎点击订阅我的优酷 如果刚学Android,不知道怎么写点击事件可以跳转,传送门 要想在Android中使用lambda语法 ...
- 一篇文章教会你使用Java8中的Lambda表达式
简介 Java 8为开发者带来了许多重量级的新特性,包括Lambda表达式,流式数据处理,新的Optional类,新的日期和时间API等.这些新特性给Java开发者带来了福音,特别是Lambda表达式 ...
- flink中使用lambda表达式
flink中使用lambda表达式 1.使用lambda的一个示例 2.使用上面这种写法通常或得到如下错误 3.解决方案 4.建议 5.完整代码 在 java8中有一种新的语法糖,即 lambda表达 ...
随机推荐
- Python练习笔记——利用递归求年龄,第五个比第四个大2岁...
现在有五个人, 第五个人比第四个人大两岁,18 第四个人比第三个人大两岁,16 第三个人比第二个人大两岁,14 第二个人比第一个人大两岁,12 第一个人现10岁, 10 ...
- 关系数据库元数据处理类(一) 创建MSSQL元数据具体处理类
public class SqlServer : BaseMetadata { public SqlServer(string connectionString) : base(new DbUtili ...
- Unix环境高级编程(八)进程关系
本章看后给人似懂非懂的感觉,主要是不知道实际当中如何去使用.通过前面几章的学习,每个进程都有一个父进程,当子进程终止时,父进程得到通知并取得子进程的退出状态.先将本章基本的知识点总结如下,日后再看时候 ...
- uva 116 - Unidirectional TSP (动态规划)
第一次做动规题目,下面均为个人理解以及个人方法,状态转移方程以及状态的定义也是依据个人理解.请过路大神不吝赐教. 状态:每一列的每个数[ i ][ j ]都是一个状态: 然后定义状态[ i ][ j ...
- yield与send实现协程操作
yield与send实现协程操作 之前我们说过,在函数内部含有yield语句即称为生成器. 下面,我们来看看在函数内部含有yield语句达到的效果.首先,我们来看看以下代码: def foo(): w ...
- tomcat6的编译和导入myeclipse
声明:近期在学习tomcat6的源代码,网上搜索了些相关的资料,并自己操作了下进行了对应的汇总.如今总结例如以下 本文目的:编译tomcat6源代码+导入tomcat6源代码到myeclipse 測试 ...
- LaTeX多文件编译的方法总结
LaTeX多文件编译的方法总结 在编写LaTeX文档的时候,由于文档的section较多,或者section的编写时间各不相同,我们可能碰到如下问题: 1.由于想分开编写各个section 2.pre ...
- 计算机通信协议之OSI参考模型
OSI参考模型 在OSI参考模型之前人类对计算机结构的研究就已经进行了太多的讨论,最终通过了作为通信协议设计指标的OSI参考模型.这个协议将通信协议中必要的功能分成了七个部分.通过这些分层使得那些比较 ...
- unity3d 通过添加rigidBody来指明物体是动态的,以避免cache开销
unity3d 通过添加rigidBody来指明物体是动态的,以避免cache开销. 如果不添加rigidBody,则unity会认为它是静态的,会对物理计算进行cache,但如果此物体实际上tran ...
- I2C三态门Verilog
http://www.blogbus.com/uyarotxb-logs/206932748.html inout作为输出端口时三态门为选通状态,inout作为输入端口时三态门为高阻态,可通过 ...