一文掌握 Lambda 表达式
本文将介绍 Java 8 新增的 Lambda 表达式,包括 Lambda 表达式的常见用法以及方法引用的用法,并对 Lambda 表达式的原理进行分析,最后对 Lambda 表达式的优缺点进行一个总结。
1. 概述
Java 8 引入的 Lambda 表达式的主要作用就是简化部分的写法。
能够使用 Lambda 表达式的一个重要依据是必须有相应的函数接口。所谓函数接口,是指内部有且仅有一个抽象方法的接口。
Lambda 表达式的另一个依据是类型推断机制。在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指名。
2. 常见用法
2.1 无参函数的简写
无参函数就是没有参数的函数,例如 Runnable
接口的 run()
方法,其定义如下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
在 Java 7 及之前版本,我们一般可以这样使用:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello");
System.out.println("Jimmy");
}
}).start();
从 Java 8 开始,无参函数的匿名内部类可以简写成如下方式:
() -> {
执行语句
}
这样接口名和函数名就可以省掉了。那么,上面的示例可以简写成:
new Thread(() -> {
System.out.println("Hello");
System.out.println("Jimmy");
}).start();
当只有一条语句时,我们还可以对代码块进行简写,格式如下:
() -> 表达式
注意这里使用的是表达式,并不是语句,也就是说不需要在末尾加分号。
那么,当上面的例子中执行的语句只有一条时,可以简写成这样:
new Thread(() -> System.out.println("Hello")).start();
2.2 单参函数的简写
单参函数是指只有一个参数的函数。例如 View
内部的接口 OnClickListener
的方法 onClick(View v)
,其定义如下:
public interface OnClickListener {
/**
* Called when a view has been clicked.
*
* @param v The view that was clicked.
*/
void onClick(View v);
}
在 Java 7 及之前的版本,我们通常可能会这么使用:
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
v.setVisibility(View.GONE);
}
});
从 Java 8 开始,单参函数的匿名内部类可以简写成如下方式:
([类名 ]变量名) -> {
执行语句
}
其中类名是可以省略的,因为 Lambda 表达式可以自己推断出来。那么上面的例子可以简写成如下两种方式:
view.setOnClickListener((View v) -> {
v.setVisibility(View.GONE);
});
view.setOnClickListener((v) -> {
v.setVisibility(View.GONE);
});
单参函数甚至可以把括号去掉,官方也更建议使用这种方式:
变量名 -> {
执行语句
}
那么,上面的示例可以简写成:
view.setOnClickListener(v -> {
v.setVisibility(View.GONE);
});
当只有一条语句时,依然可以对代码块进行简写,格式如下:
([类名 ]变量名) -> 表达式
类名和括号依然可以省略,如下:
变量名 -> 表达式
那么,上面的示例可以进一步简写成:
view.setOnClickListener(v -> v.setVisibility(View.GONE));
2.3 多参函数的简写
多参函数是指具有两个及以上参数的函数。例如,Comparator
接口的 compare(T o1, T o2)
方法就具有两个参数,其定义如下:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
}
在 Java 7 及之前的版本,当我们对一个集合进行排序时,通常可以这么写:
List<Integer> list = Arrays.asList(1, 2, 3);
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
从 Java 8 开始,多参函数的匿名内部类可以简写成如下方式:
([类名1 ]变量名1, [类名2 ]变量名2[, ...]) -> {
执行语句
}
同样类名可以省略,那么上面的例子可以简写成:
Collections.sort(list, (Integer o1, Integer o2) -> {
return o1.compareTo(o2);
});
Collections.sort(list, (o1, o2) -> {
return o1.compareTo(o2);
});
当只有一条语句时,依然可以对代码块进行简写,格式如下:
([类名1 ]变量名1, [类名2 ]变量名2[, ...]) -> 表达式
此时类名也是可以省略的,但括号不能省略。如果这条语句需要返回值,那么 return
关键字是不需要写的。
因此,上面的示例可以进一步简写成:
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
最后呢,这个示例还可以简写成这样:
Collections.sort(list, Integer::compareTo);
咦,这是什么特性?这就是我们下面要讲的内容:方法引用。
3. 方法引用
方法引用也是一个语法糖,可以用来简化开发。
在我们使用 Lambda 表达式的时候,如果 “->” 的右边要执行的表达式只是调用一个类已有的方法,那么就可以用「方法引用」来替代 Lambda 表达式。
- 引用静态方法;
- 引用对象的方法;
- 引用类的方法;
- 引用构造方法。
下面按照这 4 类分别进行阐述。
3.1 引用静态方法
当我们要执行的表达式是调用某个类的静态方法,并且这个静态方法的参数列表和接口里抽象函数的参数列表一一对应时,我们可以采用引用静态方法的格式。
假如 Lambda 表达式符合如下格式:
([变量1, 变量2, ...]) -> 类名.静态方法名([变量1, 变量2, ...])
我们可以简写成如下格式:
类名::静态方法名
注意这里静态方法名后面不需要加括号,也不用加参数,因为编译器都可以推断出来。下面我们继续使用 2.3 节的示例来进行说明。
首先创建一个工具类,代码如下:
public class Utils {
public static int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
}
注意这里的 compare()
函数的参数和 Comparable
接口的 compare()
函数的参数是一一对应的。然后一般的 Lambda 表达式可以这样写:
Collections.sort(list, (o1, o2) -> Utils.compare(o1, o2));
如果采用方法引用的方式,可以简写成这样:
Collections.sort(list, Utils::compare);
3.2 引用对象的方法
当我们要执行的表达式是调用某个对象的方法,并且这个方法的参数列表和接口里抽象函数的参数列表一一对应时,我们就可以采用引用对象的方法的格式。
假如 Lambda 表达式符合如下格式:
([变量1, 变量2, ...]) -> 对象引用.方法名([变量1, 变量2, ...])
我们可以简写成如下格式:
对象引用::方法名
下面我们继续使用 2.3 节的示例来进行说明。首先创建一个类,代码如下:
public class MyClass {
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
}
当我们创建一个该类的对象,并在 Lambda 表达式中使用该对象的方法时,一般可以这么写:
MyClass myClass = new MyClass();
Collections.sort(list, (o1, o2) -> myClass.compare(o1, o2));
注意这里函数的参数也是一一对应的,那么采用方法引用的方式,可以这样简写:
MyClass myClass = new MyClass();
Collections.sort(list, myClass::compare);
此外,当我们要执行的表达式是调用 Lambda 表达式所在的类的方法时,我们还可以采用如下格式:
this::方法名
例如我在 Lambda 表达式所在的类添加如下方法:
private int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
当 Lambda 表达式使用这个方法时,一般可以这样写:
Collections.sort(list, (o1, o2) -> compare(o1, o2));
如果采用方法引用的方式,就可以简写成这样:
Collections.sort(list, this::compare);
3.3 引用类的方法
引用类的方法所采用的参数对应形式与上两种略有不同。如果 Lambda 表达式的 “->” 的右边要执行的表达式是调用的 “->” 的左边第一个参数的某个实例方法,并且从第二个参数开始(或无参)对应到该实例方法的参数列表时,就可以使用这种方法。
可能有点绕,假如我们的 Lambda 表达式符合如下格式:
(变量1[, 变量2, ...]) -> 变量1.实例方法([变量2, ...])
那么我们的代码就可以简写成:
变量1对应的类名::实例方法名
还是使用 2.3 节的例子, 当我们使用的 Lambda 表达式是这样时:
Collections.sort(list, (o1, o2) -> o1.compareTo(o2));
按照上面的说法,就可以简写成这样:
Collections.sort(list, Integer::compareTo);
3.4 引用构造方法
当我们要执行的表达式是新建一个对象,并且这个对象的构造方法的参数列表和接口里函数的参数列表一一对应时,我们就可以采用「引用构造方法」的格式。
假如我们的 Lambda 表达式符合如下格式:
1
([变量1, 变量2, ...]) -> new 类名([变量1, 变量2, ...])
我们就可以简写成如下格式:
类名::new
下面举个例子说明一下。Java 8 引入了一个 Function
接口,它是一个函数接口,部分代码如下:
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
// 省略部分代码
}
我们用这个接口来实现一个功能,创建一个指定大小的 ArrayList
。一般我们可以这样实现:
Function<Integer, ArrayList> function = new Function<Integer, ArrayList>() {
@Override
public ArrayList apply(Integer n) {
return new ArrayList(n);
}
};
List list = function.apply(10);
使用 Lambda 表达式,我们一般可以这样写:
Function<Integer, ArrayList> function = n -> new ArrayList(n);
使用「引用构造方法」的方式,我们可以简写成这样:
Function<Integer, ArrayList> function = ArrayList::new;
4. 自定义函数接口
自定义函数接口很容易,只需要编写一个只有一个抽象方法的接口即可,示例代码:
@FunctionalInterface
public interface MyInterface<T> {
void function(T t);
}
上面代码中的 @FunctionalInterface
是可选的,但加上该注解编译器会帮你检查接口是否符合函数接口规范。就像加入 @Override
注解会检查是否重写了函数一样。
5. 实现原理
经过上面的介绍,我们看到 Lambda 表达式只是为了简化匿名内部类书写,看起来似乎在编译阶段把所有的 Lambda 表达式替换成匿名内部类就可以了。但实际情况并非如此,在 JVM 层面,Lambda 表达式和匿名内部类其实有着明显的差别。
5.1 匿名内部类的实现
匿名内部类仍然是一个类,只是不需要我们显式指定类名,编译器会自动为该类取名。比如有如下形式的代码:
public class LambdaTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
}).start();
}
}
编译之后将会产生两个 class 文件:
LambdaTest.class
LambdaTest$1.class
使用 javap -c LambdaTest.class
进一步分析 LambdaTest.class
的字节码,部分结果如下:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class com/example/myapplication/lambda/LambdaTest$1
7: dup
8: invokespecial #4 // Method com/example/myapplication/lambda/LambdaTest$1."<init>":()V
11: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: invokevirtual #6 // Method java/lang/Thread.start:()V
17: return
可以发现在 4: new #3
这一行创建了匿名内部类的对象。
5.2 Lambda 表达式的实现
接下来我们将上面的示例代码使用 Lambda 表达式实现,代码如下:
public class LambdaTest {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello World")).start();
}
}
此时编译后只会产生一个文件 LambdaTest.class
,再来看看通过 javap 对该文件反编译后的结果:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
从上面的结果我们发现 Lambda 表达式被封装成了主类的一个私有方法,并通过 invokedynamic
指令进行调用。
因此,我们可以得出结论:Lambda 表达式是通过 invokedynamic
指令实现的,并且书写 Lambda 表达式不会产生新的类。
既然 Lambda 表达式不会创建匿名内部类,那么在 Lambda 表达式中使用 this
关键字时,其指向的是外部类的引用。
6. 优缺点
优点:
- 可以减少代码的书写,减少匿名内部类的创建,节省内存占用。
- 使用时不用去记忆所使用的接口和抽象函数。
缺点:
- 易读性较差,阅读代码的人需要熟悉 Lambda 表达式和抽象函数中参数的类型。
- 不方便进行调试。
参考
“不积跬步,无以至千里”,希望未来的你能:有梦为马 随处可栖!加油,少年!
关注公众号:「Java 知己」,每天更新Java知识哦,期待你的到来!
- 发送「Group」,与 10 万程序员一起进步。
- 发送「面试」,领取BATJ面试资料、面试视频攻略。
- 发送「玩转算法」,领取《玩转算法》系列视频教程。
- 千万不要发送「1024」...
一文掌握 Lambda 表达式的更多相关文章
- Java :一文掌握 Lambda 表达式
本文将介绍 Java 8 新增的 Lambda 表达式,包括 Lambda 表达式的常见用法以及方法引用的用法,并对 Lambda 表达式的原理进行分析,最后对 Lambda 表达式的优缺点进行一个总 ...
- Java8 Lambda表达式详解手册及实例
先贩卖一下焦虑,Java8发于2014年3月18日,距离现在已经快6年了,如果你对Java8的新特性还没有应用,甚至还一无所知,那你真得关注公众号"程序新视界",好好系列的学习一下 ...
- 一文带你深入了解 Lambda 表达式和方法引用
前言 尽管目前很多公司已经使用 Java8 作为项目开发语言,但是仍然有一部分开发者只是将其设置到 pom 文件中,并未真正开始使用.而项目中如果有8新特性的写法,例如λ表达式.也只是 Idea Al ...
- 一文搞懂Java8 Lambda表达式(附带视频教程)
Lambda表达式介绍 Java 8的一个大亮点是引入Lambda表达式,使用它设计的代码会更加简洁.通过Lambda表达式,可以替代我们以前经常写的匿名内部类来实现接口.Lambda表达式本质是一个 ...
- 匿名函数 lambda表达式(lambda expression)
阅读g2log时,发现有两行代码居然看不懂. 1. auto bg_call = [this, log_directory]() {return pimpl_->backgroundChang ...
- Java Lambda表达式初探
Java Lambda表达式初探 前言 本文受启发于Trisha Gee在JavaOne 2016的主题演讲Refactoring to Java 8. Java 8已经发行两年多,但很多人仍然在使用 ...
- c++11 新特性之lambda表达式
写过c#之后,觉得c#里的lambda表达式和delegate配合使用,这样的机制用起来非常爽.c++11也有了lambda表达式,形式上有细小的差异.形式如下: c#:(input paramete ...
- Java8初体验(一)lambda表达式语法
感谢同事[天锦]的投稿.投稿请联系 tengfei@ifeve.com 本文主要记录自己学习Java8的历程,方便大家一起探讨和自己的备忘.因为本人也是刚刚开始学习Java8,所以文中肯定有错误和理解 ...
- python3 入门 (三) 函数与lambda表达式、闭包
函数 是组织好的.可重复使用的.用来实现单一或相关联功能的代码段. 函数代码块以def关键词开头,后接函数标识符名称和圆括号() 任何传入参数和自变量必须放在圆括号中间.圆括号之间可以用于定义参数 函 ...
随机推荐
- 小白学 Python 爬虫(22):解析库 Beautiful Soup(下)
人生苦短,我用 Python 前文传送门: 小白学 Python 爬虫(1):开篇 小白学 Python 爬虫(2):前置准备(一)基本类库的安装 小白学 Python 爬虫(3):前置准备(二)Li ...
- 在 ASP.NET Core 中使用 AutoMapper 使 Entity 和 Resource 之间进行映射
目录 从 NuGet 安装 AutoMapper 添加 Entity类 和 Resource类 添加一个 Profile文件,配置映射关系 在Startup中对AutoMapper进行注册 在项目中使 ...
- 《MySQL数据库》MySQL数据库安装(linux)
1. 下载安装包: 百度网盘:链接: https://pan.baidu.com/s/1toGl8O9gMBpDWn0mHWwFyg 提取码: i51g 官网下载:https://dev.mysql ...
- android开发针对小米、三星、华为8.0+系统个别型号打开应用闪退
最近开发中有个别客户反馈新换的三星.小米或者华为手机打开应用就闪退,而且是个别型号.针对这种情况特别查阅了一些资料,原因是8.0+系统的手机不允许后台创建服务,那么怎么修改呢,请看代码: 1.修改启动 ...
- Maven使用教程二:nexus私服搭建及使用
nexus安装 从nexus官网 下载最新的安装包 1.打开命令行,切换到nexus-3.2.1-01/bin目录下,回车.例:C:\Nexus\nexus-3.2.1-01\bin 2.输入:nex ...
- 搭建API Mock
所需环境 Node.js + MySQL 5.7+ Redis 4.0+ Node.js 安装 .要安装nvm,需要安装构建源包所需的工具,CentOS 上安装,用这些命令来安装构建工具: sudo ...
- linux 定时备份数据库
说明 检查Crontab是否安装 若没有 需要先安装Crontab定时工具 安装定时工具参考(https://www.cnblogs.com/shaohuixia/p/5577738.html) 需要 ...
- linux 用户,组
权限: 所谓的权限是,由用户启动的进程,或者由操作系统启动的进程,可以访问哪些文件,不可以访问哪些文件. 进程太多了,不可能为每个进程定义权限对吧,所以进程的权限来自于启动进程的用户. 用户有哪些权限 ...
- 记录MyBatis text类型 查询 更新 数据是null
数据库表里面存在text或者blob字段.逆向工程自动生成的MyBatis的xml中会多出几个以withBlobs结尾的方法和resultMap 此时查询数据或者更新数据的使用仍然使用selectBy ...
- 面试连环炮系列(七):HashMap的put操作做了什么
HashMap的put操作做了什么? HashMap的是由数组和链表构成的,JDK7之后加入了红黑树处理哈希冲突.put操作的步骤是这样的: 根据key值计算出哈希值作为数组下标.如果数组的这个位置是 ...