Java Lambda简明教程(一)
Lambda表达式背景
许多热门的编程语言如今都有一个叫做lambda或者闭包的语言特性,包括比较经典的函数式编程语言Lisp,Scheme,也有稍微年轻的语言比如JavaScript,Python,Ruby,Groovy,Scale,C#,甚至C++也有Lambda表达式。一些语言是运行在java虚拟机上,作为虚拟机最具代表的语言java当然也不想落后。
究竟什么是Lambda表达式?
Lambda表达式的概念来自于Lambda演算,下面是一个java lambda的简单例子,
(int x) -> { return x+1; }
简单来看lambda像一个没有名字的方法,它具有一个方法应该有的部分:参数列表int x,方法body return x+1,和方法相比lambda好像缺少了一个返回值类型、异常抛出和名字。返回值类型和异常是通过编译器在方法体中推导出来,在上面这个例子中返回值类型是int,没有抛出异常。真正缺少的就是一个名字,从这个角度来看,lambda表达式是一种匿名方法。
Lambda表达式和匿名内部类
从上面的分析可以看出lambda和java内部类的特性有点相似,匿名内部类不只是一个方法,而是一个包含一个或多个方法的类,他们的作用都是一样的,都是作为方法的参数传递,我从JDK源码中提取出来listFiles(FileFilter) 方法:
public File[] listFiles(FileFilter filter) {
String ss[] = list();
if (ss == null) return null;
ArrayList<File> files = new ArrayList<>();
for (String s : ss) {
File f = new File(s, this);
if ((filter == null) || filter.accept(f))
files.add(f);
}
return files.toArray(new File[files.size()]);
}
listFilter方法接收一个功能接口作为参数,在这里是FileFilter接口:
public interface FileFilter {
boolean accept(File pathname);
}
fileFilter接收一个File对象返回一个boolean值,listFiles方法把Filter应用到所有的File对象接收 那些accept返回true的文件。对于listFiles方法来讲我们必须传递一个函数式接口给他,这是FileFileter的一个实现,一般我们通过匿名类来完成:
File myDir = new File("/home/user/files");
if (myDir.isDirectory()) {
File[] files = myDir.listFiles(
new FileFilter() {
public boolean accept(File f) { return f.isFile(); }
}
);
}
我们现在可以用lambda来实现:
File myDir = new File("/home/user/files");
if (myDir.isDirectory()) {
File[] files = myDir.listFiles(
(File f) -> { return f.isFile(); }
);
}
这两种情况我们都是传递了一个函数式接口给方法就像传递对象一样,我们使用代码就像使用数据一样,使用匿名类我们实际上传递了一个对象给方法,使用lambda不再需要创建对象,我们只需要把lambda代码传递给方法。
除了传递lambda之外我们还可以传递一个方法引用,比如:
File[] files = myDir.listFiles( File::isFile );
Lambda表达式的表示
在之前的例子,我们使用lambda表达式定义了一个函数,我们可以把它作为参数传递给一个方法,方法把它当成一个对象来使用,lambda表达式有函数和对象的一些属性,看你从什么角度来看:
- 从概念来讲,lambda表达式是一个匿名函数,它有签名和方法体但是没有名字
- 当lambda表达式作为参数传递给方法时,接收方法把它当对象使用,在
listFiles方法内部,lambda表达式是一个对象的引用,在这里lambda表达式是一种常规的对象,比如有地址和类型。
从实际的角度来分析,lambda对象是由编译期和运行时系统来创建的,这就允许编译期进行优化而使用者不需要关心具体细节,编译器从lambda表达式的上下文环境来获取lambda对象的语义类型,但是编译期并不创建那个对象而是直到运行时由虚拟机动态创建,这里说的动态创建是指调用invokedynamic字节码指令来创建。使用动态创建可以推迟对象的创建到对象第一次被使用时,如果你只是定义了lambda表达式而从未使用,它的类型和对象都不会创建。
函数式接口
整个魔幻之处就在于类型的推导,这个类型称为目标类型,运行时系统动态创建的类型是目标类型的子类型。之前的那个例子我们看到目标类型是FileFilter,在例子中我们定义了一个lambda表达式把它传递给listFiles方法,然后listFiles方法把它作为FileFilter子类的一个对象来使用。这里看起来好像有点神奇,我们并没有声明lambda表达式实现了FileFilter接口,listFiles方法也没有表明它很愉快的接收了lambda表达式,它只是需要一个FileFilter的子类的对象,这是如何工作的?
这里面的魔术在于编译期执行了类型推导,编译器根据lambda表达式的上下文来决定需要什么类型的对象,然后编译器观察lambda表达式是否兼容需要的类型。如果Java是一种函数式编程语言的话lambda表达式最自然的类型就是某种函数式类型,用来描述函数的一种特殊类型。函数式类型仅仅描述了函数的签名比如(int,int)->boolean.但是Java不是函数式编程语言因此没有函数式类型,语言的设计者可以选择添加一种新的类型,由于他们不想给Java的类型系统引入太多的改变,因此他们尝试寻找一种办法来集成lambda表达式到语言中而不需要添加函数式类型。
结果他们使用函数式接口来代替,函数式接口是只有一个方法的接口,这样的接口在JDK里有很多,比如经典的Runnable接口,它只有一个方法void run(),还有很多其他的,比如Readable,Callable,Iterable,closeable,Flushnable,Formattable,Comparable,Comparator,或者我们前面提到的FileFilter接口。函数是接口和lambda表达式奕扬都只有一个方法,语言的设计者决定让编译器把lambda表达式转换成匹配的函数式接口。这种转换通常是自动的。比如我们前面提到的(File f) -> { return f.isFile(); },编译器知道listFiles方法的签名,因此我们需要的类型就是FileFilter,FileFilter是这样的:
public interface FileFilter { boolean accept(File pathname); }
FileFilter仅仅需要一个方法因此它是函数式接口类型,我们定义的lambda表达式有一个相匹配的签名,接收一个File对象,返回一个boolean值,不抛出检查的异常,因此编译器把lambda表达式转换成函数式接口FileFilter类型。
假如我们有下面两个函数式接口:
public interface FileFilter { boolean accept(File pathname); }
public interface Predicate<T> { boolean test(T t); }
我们的lambda表达式兼容两种函数式接口类型:
FileFilter filter = (File f) -> { return f.isFile(); };
Predicate<File> predicate = (File f) -> { return f.isFile(); };
filter = predicate;//错误,不兼容的类型
当我们试图给两个变量相互赋值时编译器会报错,虽然两个变量都是同一个lambda表达式,原因很简单两个变量是不同的类型。也有可能出现编译器无法判断匹配的函数式接口类型,比如这个例子:
Object ref = (File f) -> { return f.isFile(); };
这个赋值语句的上下文没有提供足够的信息来转换,因此编译器会报错,解决这个问题最简单的方法就是添加一个类型转换:
Object ref = (FileFilter) (File f) -> { return f.isFile(); };
Lambda表达式和匿名内部类的区别
Lambda表达式出现在我们通常需要匿名内部类的地方,在很多场合他们是可以互换的。但是他们还是有几个区别:
语法
匿名类一般这样编写:
Fi le[] fs = myDir.lis tFiles(
new FileFilter() {
public boolean accept(File f) { return f.isFile(); }
}
);
而Lambda表达式有多种形式:
File[] files = myDir.listFiles( (File f) -> {return f.isFile();} );
File[] files = myDir.listFiles( f -> f.isFile() );
F ile[] fil e s = myDir.listFiles( File::isFile );
运行时成本
匿名类相对Lambda表达式来讲多了一些成本,使用匿名类或造成新类型的创建、新类型对象的创建。运行时匿名内需要:
- 类加载
- 内存分配、对象初始化
- 调用非静态方法
Lambda表达式需要函数式接口的转换和最终的调用,类型推导发生在编译期,不需要运行时消耗,之前提到过,lambda对象的创建是通过字节码指令invokedynamic来完成的,减少了类型和实例的创建消耗。
变量绑定
匿名类可以访问外部域的final变量,如下所示:
void method() {
final int cnt = 16;
Runnable r = new Runnable() {
public void run() {
System.out.println("count: " + cnt);
}
};
Thread t = new Thread(r);
t.start();
cnt++;// error: cnt is final
}
对于lambda表达式,cnt变量不需要显式声明为final的,一旦变量在lambda中使用编译期会自动把它当成是final的变量,换句话说在lambda中使用的外部域变量是隐式final的,
void method() {
int cnt = 16;
Runnable r = () -> { System.out.println("count: " + cnt);
};
Thread t = new Thread(r);
t.start();
cnt++;// error: cnt is implicitly final
}
从java8开始匿名内部类也不需要再显式声明final类,编译器会自动把它当成是final。
作用域
匿名内部类是一个类,也就是说它自己引入了一个作用域,你可以在里面定义变量,而lambda表达式没有自己的作用域。
void method() {
int cnt = 16;
Runnable r = new Runnable() {
public void run() { int cnt = 0; // fine
System.out.println("cnt is: " + cnt); }
};
}
lambda表达式:
void method() {
int cnt = 16;
Runnable r = () -> { int cnt = 0; // error: cnt has already been defined
System.out.println("cnt is: " + cnt);
};
}
不同的作用域规则对于this和super关键字有不同的效果,在匿名类中this表示匿名类对象本身的引用,super表示匿名类的父类。在lambda表达式this和super关键字意思和外部域中this和super的意思一样,this一般是包含它的那个对象,super表示包含它的类的父类。
下一篇我将介绍lambda表达式在集合中的使用。
Java Lambda简明教程(一)的更多相关文章
- Java Lambda表达式教程与示例
Lambda表达式是Java 8中引入的一个新特性.一个lambda表达式是一个匿名函数,而且这个函数没有名称且不属于任何类.lambda表达式的概念最初是在LISP编程语言中引入的. Java La ...
- Java泛型简明教程
泛型是Java SE 5.0中引入的一项特征,自从这项语言特征出现多年来,我相信,几乎所有的Java程序员不仅听说过,而且使用过它.关于Java泛型的教程,免费的,不免费的,有很多.我遇到的最好的教材 ...
- Java 8简明教程
本文由 ImportNew 欢迎阅读我编写的Java 8介绍.本教程将带领你一步一步地认识这门语言的新特性.通过简单明了的代码示例,你将会学习到如何使用默认接口方法,Lambda表达式,方法引用和重复 ...
- Java 8 简明教程
欢迎阅读我编写的 Java 8 介绍.本教程将带领你一步步认识这门语言的所有新特性.通过简单明了的代码示例,你将会学习到如何使用默认接口方法,Lambda表达式,方法引用和可重复注解.在这篇教程的最后 ...
- 30 分钟 Java Lambda 入门教程
Lambda简介 Lambda作为函数式编程中的基础部分,在其他编程语言(例如:Scala)中早就广为使用,但在Java领域中发展较慢,直到java8,才开始支持Lambda. 抛开数学定义不看,直接 ...
- Java网络编程简明教程
Java网络编程简明教程 网络编程 计算机网络相关概念 计算机网络是两台或更多的计算机组成的网络,同一网络内的任意两台计算机可以直接通信,所有计算机必须遵循同一种网络协议. 互联网 互联网是连接计算 ...
- Java8简明教程(转载)
ImportNew注:有兴趣第一时间学习Java 8的Java开发者,欢迎围观<征集参与Java 8原创系列文章作者>. 以下是<Java 8简明教程>的正文. “Java并没 ...
- Java 简明教程
本文为 Java 的快速简明教程,主要用于快速了解.学习和复习java的语法特点. // 单行注释 /* 多行注释 */ /** JavaDoc(Java文档)注释是这样的.可以用来描述类和类的属性. ...
- Lisp简明教程
此教程是我花了一点时间和功夫整理出来的,希望能够帮到喜欢Lisp(Common Lisp)的朋友们.本人排版很烂还望多多海涵! <Lisp简明教程>PDF格式下载 <Lisp简明教程 ...
随机推荐
- ZOJ-3720 Magnet Darts 计算几何,概率
题目链接:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=3720 题意:在一个矩形区域投掷飞镖,每个整点有磁性,每个点的磁性 ...
- Java NIO 缓冲技术详解
缓冲区(buffer)是从即将写入通道(channel)或刚刚从通道中读出的一段数据.它是一个持有数据,并扮演NIO通道端点的对象.缓冲区为数据访问和读写过程提供正式机制. 它是NIO和老版Java ...
- IOS 10适配https 包含对于一些http的一些兼容配置
iOS10 从2017年1月1日起苹果提出所有新提交的App默认不允许使用NSAllowsArbitraryLoads来绕过ATS的限制,也就是说强制我们用HTTPS,如果不这样的话提交App可能会被 ...
- Apache CXF框架结构和基本原理(转)
原文链接:http://blog.sina.com.cn/s/blog_6182547f01017pak.html CXF旨在为服务创建必要的基础设施,它的整体架构主要由以下几个部分组成: 1.Bus ...
- 数据库数据导入导出系列之五 C#实现动态生成Word(转)
1. 一个控制台例子,实现动态生成Word. 首先,添加引用:COM->Microsoft Word 11.0 Object Library. 2. 介绍几篇牛人写的关于操作Word的文章 [分 ...
- PowerShell脚本传递参数
在编写PowerShell脚本的时候,可以通过给变量赋值的方法输出想要的结果,但这样的话,需要改动脚本内容.其实也可以在脚本中定义参数,然后再在执行脚本的时候对参数赋值,而无需改动脚本内容. 在Pow ...
- Bootstrap-风格的下拉按框:Bootstrap Select
Bootstrap Select 是一个jQuery插件,提供了Bootstrap 风格的下拉选择框.拥有许多自定义的选项,可多选. 效果图: 源代码: <select class=" ...
- play wav sound
播放 WAV文件 string s = @"D:\Administrator\安装文件\完美世界国际版\patcher\skin\sounds\click.wav&q ...
- xargs i I{} 参数说明
find . -type f | xargs -I{} md5sum {} find . -type f -name "*.txt" | xargs -i cp {} /tmp/k ...
- Java中各种排序算法
package org.rut.util.algorithm.support; import org.rut.util.algorithm.SortUtil; /** * @author treero ...