持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第26天,点击查看活动详情
之前的文章介绍了Java 8 函数式编程的相关概念以及使用。在那些文章中,我们或多或少都会用到一些lambda 表达式。可能不熟悉的人看到之后对lambda 表达式不知所以,所以这里我们来介绍一下lambda 表达式。
lambda 表示式的概念
lambda 表达式也是Java 8 引入的一个全新的特性。lambda 表达式也可以被称为闭包。
lambda 表达式的一个最明显或者说最基本的特征就是允许函数作为一个方法的参数(即,将函数当作参数传递进方法中)。
使用lambda 表达式最明显的表现就是可以使代码变的更加简洁紧凑。
但是虽然看起来lambda 表达式很“新颖”,很“不一样”,但是它本质只是一个语法糖。
它的生效流程就是由编译器对表达式进行推断,并将其转换包装成常规java 代码。所以说,在我们充分了解了lambda 表达式的语法的基础上,我们就可以使用更少的代码来实现相同的功能了。
但是简洁也有简洁的缺点,比如说我们在日常debug 的过程中,针对lambda 表达式的代码就很难一步一步走进去看其执行流程;另外,lambda 表达式虽然简洁,但是对于新手来说却很难懂,难以调试,对于维护人员来说,相比于普通java 代码,维护起来也比较难受。一般来说,代码行数很多的逻辑就建议不要使用lambda 表达式了。
lambda 表达式的特点
首先我们明确一下lambda 表达式的基本语法(之后再讲具体的使用方法):
(parameters) -> expression
(parameters) ->{ statements; }
在java 8 中,我们是被允许通过lambda 表达式来代替功能接口的(功能接口就是实现某个逻辑的接口)。
所以说,lambda 表达式组成的元素其实和正常java 语法的方法 method一样,它需要一个参数列表和一个使用这些参数的主体,这个“主体”可以是一个表达式或一个代码块。
初识lambda 表达式
Lambda 表达式其实是一个代码块的描述(或者叫匿名方法的lambda 表达方式),对于一个lambda 表达式,可以像匿名方法一样,将其作为参数传递给某个方法,然后在方法被调用之后执行其中的逻辑。这个方法可以是构造方法,也可以是普通方法。参考下面这段代码:
() -> System.out.println("初识lambda 表达式");
如果第一次看到这种写法可能会很迷惑,括号是啥,那个箭头又是什么?那么我们来从左到右解释一下。
- 代码中的括号 其实是lambda 表达式的参数列表,如果不需要参数,那么括号为空即可。(上面的例子中没有参数;同时如果参数只有一个,那么可以省略这个括号)
- 代码中的“箭头->” 标识这串代码为lambda 表达式 的特定代码(也就是说,只要在java 代码中看到“->” 就知道这是lambda 表达式)。
- 后面的执行语句
System.out.println("初识lambda 表达式")就是将要执行的代码逻辑,也就是打印输出一个字符串的功能。
这里我们再使用jdk 源码中的Runnable 来作说明。Runnable 是多线程的一个基础接口,它的定义如下:
@FunctionalInterface
public interface Runnable
{
public abstract void run();
}
Runnable 接口的定义其实非常简单,它仅有一个抽象方法run();接口名称上有一个注解:@FunctionalInterface,这个注解的作用就是说明这个接口为一个“函数式接口”。
查看这个接口的代码,可以发现里面有这样一段注释:
Note that instances of functional interfaces can be created with lambda expressions, method references, or constructor references.
这段注释的意思就是通过@FunctionalInterface 标记的接口除了一些常用的创建实例的方法之外,还可以通过 lambda 表达式创建实例。
如果不使用lambda 表达式,我们创建一个线程之后,启动这个线程,代码如下:
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("lambda test");
}
}).start();
}
如果我们使用lambda 表达式,却只需要以下一行代码:
public static void main(String[] args) {
new Thread(() -> System.out.println("lambda test")).start();
}
lambda 的基本语法
每个lambda 表达式都大同小异地遵循以下语法规则:
(parameter list) -> { expression/statements }
这里我们具体地从细节方面介绍这个表达式的结构。
-
括号:() 中的“parameter list” 代表着以逗号分隔的参数。我们可以传入单个参数或者多个参数,甚至可以不传任何参数。我们可以指定参数的类型,当然了,既然这么说了,那么就代表我们也可以不指定参数的类型。如果不指定,那么java 编译器会根据上下文进行自行推断。当没有参数时,括号必须存在;当只有一个参数时,括号可以省略;当有多个参数的时候,括号也必须存在。
-
箭头:-> 最基本的作用就是作为lambda 表达式代码的标识符,看到这个符号我们就知道,这段代码是lambda 表达式代码。
-
大括号中的内容:{} 中的“expression/statements” 就是lambda 表达式的主体了。大括号中的内容可以是一行语句,也可以是多行。如果只有一行的话,那么大括号就可以省略。
lambda 表达式的基本使用场景
在清楚了lambda 表达式的基本结构之后,我们来看一下lambda 的基本使用场景。
- 作为一个方法的参数,传入到普通方法或者构造方法中,举例如下:
new Thread(() -> System.out.println("lambda test")).start();
- 生成一个变量的实例(为变量赋值),举例如下:
Runnable r = () -> { System.out.println("lambda test"); };
r.run();
- 作为一个代码块最后的return 的结果,示例如下:
return (fileName) -> fileName.toString().endsWith(suffix);
- 可以作为数组的元素,举例如下:
final PathMatcher matcher[] = {
(path -> path.toString().endsWith("abc")),
(path -> path.toString().endsWith("def"))
};
lambda 表达式的注意事项
在日常使用lambda 表达式的时候,我们经常会遇到一些报错提示,其中一个比较常见的就是表达式的作用域范围导致的。这里举例如下:
示例1
public static void main(String[] args) {
int testFlag = 10;
Runnable r = () -> {
int testFlag = 5;
for (int i = 0; i < limit; i++)
System.out.println(i);
};
}
这段代码输入到IDE 中,则会有如下图中的提示:
这个提示错误就是:变量
testFlag 已经定义过了。它在编译期间就会报出这个问题,也就是说这段代码编译就不会通过。
示例2
我们再看下面这个例子:
public static void main(String[] args) {
int testFlag = 10;
Runnable runnable = () -> {
testFlag = 5;
for (int i = 0; i < testFlag; i++) {
System.out.println(i);
}
};
}
它的错误提示是:
它的意思是在lambda 表达式中使用的变量应该是final 的。
造成这个现象的原因,是由于内部类使用外部的局部变量时,因为外部的局部变量随着生命周期的结束而销毁,但内部类的生命周期还未结束,还在使用该变量,这样子就会造成内外不一致。在jdk8 之前,需要在该变量前面加上final,但是在jdk8之后,如果不涉及到变量的引用改变,则jdk会默认加上final。
上面两个例子都说明:
- 不要在lambda 表达式主体内对方法中的局部变量进行修改,否则编译不会通过。
- lambda 表达式中使用的变量必须是final 的,这个规则和匿名内部类的使用规则一样。
这个问题发生的原因是因为Java 中有一个这样的规定:
Any local variable, formal parameter,
or exception parameter used but not declared in a lambda expression
must either be declared final or be effectively final (§4.12.4),
or a compile-time error occurs where the use is attempted.
这个意思是说,在lambda 表达式中使用到的变量,如果没有在表达式中声明,那么这个变量就需要时final 或者effectively final 的,否则就会出现编译错误。
即:lambda 表达式中使用到的变量要么是在表达式中声明的变量,要么是final 类型的变量。
细节概念介绍
在上一篇文章中我们可能会注意到其中有一个提示:Variable 'testFlag' is accessed from within inner class, needs to be final or effectively final,对于这个提示我们可以见到其中后一句话写到:final or effectively final。
effectively 的意思是“有效地;实际上”。
有的人可能不知道,final 和effectively final 的区别是什么,这里我们来讲一下。
final int flag;
flag = 1;
这种情况,对于变量flag 来说就是final 的。这种情况,如果设置flag = 2 就会报错,因为flag 是final 的,所以不能重新赋值。
int flag;
flag = 1;
如果flag 此后再未进行改动,那么这个变量flag 就是effectively final
int flag;
flag = 1;
flag = 2;
首先flag 先被赋值为1,然后又被重新赋值为2,这个时候flag 就不是effectively final 了。
我们这里贴一下那个报错:
对于上面那种情况,在我们清楚了final 和effectively final 的区别后,我们可以有一个很直观的做法,就是把 变量flag 定义为final 类型的,这样报错就不会再出现了。
但是这种情况会导致开发同学就无法在lambda 表达式中去修改变量的值了。
针对这种情况的,我们其实也有很多解决方法的,这里做如下介绍:
使用数组
使用数组的方式,需要在声明数组的时候将数组设置为final 类型,然后我们使用数组中的元素来进行变量传递,即:更改int 的值的时候是去修改数组的一个元素。
public static void main(String[] args) {
final int [] flags = {5};
Runnable r = () -> {
flags[0] = 3;
for (int i = 0; i < flags[0]; i++) {
System.out.println(i);
}
};
}
这样就可以跳过刚才的编译检查报错的问题。
把 limit 变量声明为 AtomicInteger
对于AtomicInteger 类的使用,如果读者不熟悉可以自行查阅一下。AtomicInteger 可以确保 int 值的修改是原子性的,通过这个特性我们可以避免编译过程中的报错检查。我们可以使用AtomicInteger 的set() 方法设置一个新的值,通过get() 方法获取当前的值。
public static void main(String[] args) {
final AtomicInteger flag = new AtomicInteger(5);
Runnable runnable = () -> {
flag.set(5);
for (int i = 0; i < flag.get(); i++) {
System.out.println(i);
}
};
}
实际运行一下发现这个也可以避免编译报错问题的检查。
把变量声明为static 类型
如果要把变量声明为static 类型,那么需要把变量放在方法的外部,因为static 类型的东西是和类有关系的,所以要放到类的层级。这里举例如下:
public class ModifyVariable2StaticInsideLambda {
static int flag = 5;
public static void main(String[] args) {
Runnable runnable = () -> {
flag = 3;
for (int i = 0; i < flag; i++) {
System.out.println(i);
}
};
}
}
这种方法运行之后发现也可以避免编译报错的问题,所以也是一种解决方案。