出品 | CSDN 博客
本文将阐述 Java 8 引入的 Lambda 表达式,涵盖其常见应用场景、方法引用技巧,深入剖析 Lambda 表达式的运作机制,并最终对 Lambda 表达式的利弊进行归纳总结。
概述
Java 8 新增的 Lambda 表达式功能,其核心目的是为了使某些匿名内部类的编写过程变得更加简便。
能够运用 Lambda 表达式的前提条件之一,是必须存在相应的函数接口。所谓的函数接口,指的是那种内部仅包含一个抽象方法的接口。
Lambda表达式还依赖于类型推断的机制。当上下文信息充足时,编译器能够自行推断出参数表中的数据类型,无需我们进行明确的指定。
常见用法
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;
当只有一条语句时,我们还可以对代码块进行简写,格式如下:
-> 表达式
此处所采用的是表达方式,而非完整的句子结构,因此,在表达结束时无需添加标点符号。
那么,当上面的例子中执行的语句只有一条时,可以简写成这样:
启动一个新线程,该线程执行输出"Hello"的操作,并立即开始执行。
2.2 单参函数的简写
单参函数即仅接受一个参数的函数类型。以 View 类中的 OnClickListener 接口为例,其方法 onClick(View v) 就是一个典型的单参函数,其具体定义方式如下:
定义一个名为OnClickListener的公共接口。
/**
触发于视图被点击时。
*
* @param v The view that was clicked.
*/
void onClick(View v);
}
在 Java 7 及之前的版本,我们通常可能会这么使用:
为该视图设置点击监听器,监听器由一个匿名内部类实现,该类继承自View.OnClickListener接口。
@Override
public void onClick(View v) {
v.setVisibility(View.GONE);
}
});
自 Java 8 版本起,对于单参数的匿名内部类,我们可以采用以下简化形式进行编写:
([类名 ]变量名) -> {
执行语句
}
类名在此处并非必需,Lambda 表达式具备自行识别其所属类别的功能。因此,上述示例可以以以下两种形式进行简化:
设置点击监听器,当用户点击特定视图时,将触发一个匿名内部类中的方法执行,该方法接受一个参数v,代表被点击的视图对象。
v.setVisibility(View.GONE);
});
设置点击事件监听器,当用户点击视图时,将执行以下操作:
v.setVisibility(View.GONE);
});
单参函数甚至可以把括号去掉,官方也更建议使用这种方式:
变量名 -> {
执行语句
}
那么,上面的示例可以简写成:
设置点击监听器,当用户点击时,触发一个事件处理函数。
v.setVisibility(View.GONE);
});
当只有一条语句时,依然可以对代码块进行简写,格式如下:
([类名 ]变量名) -> 表达式
类名和括号依然可以省略,如下:
变量名 -> 表达式
那么,上面的示例可以进一步简写成:
当点击事件发生时,将视图的可见性设置为不可见。
2.3 多参函数的简写
多参数函数指的是那些接受两个或更多参数的函数类型。以 Comparator 接口中的 compare(T o1, T o2) 方法为例,它便是一个包含两个参数的函数,其具体定义如下:
@FunctionalInterface
public interface Comparator {
int compare(T o1, T o2);
}
在 Java 7 及其早期版本中,进行集合排序的操作通常可以这样表述:
List创建列表list,并将数字1、2、3依次添加其中。
Collections对列表进行排序,采用了一个新的比较器对象。 {
@Override
公开函数compare,接受两个Integer类型的参数o1和o2,返回一个整数值。
return o1.compareTo(o2);
}
});
自 Java 8 版本起,对于多参数的匿名内部类,我们能够采用以下简化的形式进行编写:
([类名1]的变量[变量名1], [类名2]的变量[变量名2], 以此类推) 转换为 {
执行语句
}
同样类名可以省略,那么上面的例子可以简写成:
对列表进行排序,采用Collections.sort方法,将list作为参数传入,比较器由lambda表达式定义,接受两个Integer类型的参数o1和o2,以实现排序功能。
return o1.compareTo(o2);
});
list中的元素按照Collections.sort方法进行排序,比较操作由lambda表达式定义,该表达式接受两个对象o1和o2作为参数,以实现自定义的排序逻辑。
return o1.compareTo(o2);
});
当只有一条语句时,依然可以对代码块进行简写,格式如下:
定义一个新变量,其类型为类名1,名为变量名1,同时定义另一个新变量,其类型为类名2,名为变量名2,以此类推,最终通过表达式进行赋值。
当前情况下,类名可以被省略,不过括号却是不可或缺的。若该语句需提供返回结果,则无需使用 return 关键字。
因此,上面的示例可以进一步简写成:
.compareTo(o2));
最后呢,这个示例还可以简写成这样:
对列表进行排序,采用Collections类中的sort方法,参数为列表和Integer类的compareTo方法引用。
咦,这是什么特性?这就是我们下面要讲的内容:方法引用。
方法引用
方法引用也是一个语法糖,可以用来简化开发。
当我们在运用 Lambda 表达式时,若“->”符号右侧的执行表达式仅涉及调用一个既有的类方法,我们便可以采用「方法引用」的方式来替换 Lambda 表达式。
方法引用可以分为 4 类:
下面按照这 4 类分别进行阐述。
3.1 引用静态方法
在执行表达式时,若涉及调用某类之静态方法,且该静态方法的参数项与接口中抽象函数的参数项完全一致,我们便可以运用引用静态方法的特定格式。
假如 Lambda 表达式符合如下格式:
对变量1、变量2等进行操作,需调用类名中的静态方法名,并传入相应参数。
我们可以简写成如下格式:
类名::静态方法名
在此处,我们应留意静态方法名称后无需附加括号及参数,编译器自有能力进行推断。接下来,我们将借助2.3节中的实例进行详细阐述。
首先创建一个工具类,代码如下:
public class Utils {
public static int 进行比较操作,参数分别为 Integer 类型的 o1 和 o2,返回一个 int 类型的结果。
return o1.compareTo(o2);
}
}
请注意,此处 compare 函数的输入参数与 Comparable 接口中 compare 方法的参数是严格匹配的。随后,通常的 Lambda 表达式可以表述为:
.compare(o1, o2));
如果采用方法引用的方式,可以简写成这样:
对列表进行排序,采用Collections类中的sort方法,以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变量。
.compare(o1, o2));
这里需留意,函数的各个参数均存在一一对应的关系,因此我们可以通过方法引用的方式,将其简化如下:
MyClass myClass = new MyClass;
使用Collections.sort方法对列表进行排序,其中排序依据是调用myClass类的compare方法。
此外,若执行的表达式涉及调用Lambda表达式所属类的方法,我们可以采用以下结构:
this::方法名
例如我在 Lambda 表达式所在的类添加如下方法:
定义一个私有方法,其功能为比较两个整数值,参数分别为o1和o2,返回类型为int。
return o1.compareTo(o2);
}
当 Lambda 表达式使用这个方法时,一般可以这样写:
对列表进行排序,采用Collections.sort方法,参数为列表和自定义比较器,该比较器通过lambda表达式实现,具体为:compare函数比较两个对象o1和o2。
如果采用方法引用的方式,就可以简写成这样:
list中的元素按照本类的比较方法进行排序,使用Collections.sort方法,并指定当前对象的比较器。
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、变量2等输入后,将生成一个新的类实例,其构造函数接收相同的变量列表。
我们就可以简写成如下格式:
类名::new
以下以实例进行阐释,Java 8 版本中新增了 Function 接口,该接口属于函数式接口范畴,其部分代码呈现如下:
@FunctionalInterface
public interface Function {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
// 省略部分代码
}
通过这个接口,我们旨在实现一项功能,即构建一个具有特定尺寸的 ArrayList。通常情况下,我们可以采取以下方式来完成这一任务:
Function function = new Function {
@Override
public ArrayList apply(Integer n) {
return new ArrayList(n);
}
};
List list = function.apply(10);
使用 Lambda 表达式,我们一般可以这样写:
Function定义函数时,参数n的值将决定一个新创建的ArrayList的大小,即ArrayList的容量将与n相等。
使用「引用构造方法」的方式,我们可以简写成这样:
Function function = ArrayList::new;
自定义函数接口
创建自定义函数接口相当简便,只需编写一个包含单一抽象方法的接口即可,具体示例代码如下:
@FunctionalInterface
public interface MyInterface {
void function(T t);
}
在上述代码中,标注 @FunctionalInterface 是非强制性的,然而,若你添加此注解,编译器便会协助验证该接口是否满足函数式接口的标准。这类似于使用 @Override 注解来确保函数已被正确重写。
实现原理
通过前面的阐述,我们了解到 Lambda 表达式的主要目的是为了简化匿名内部类的编写。它似乎在编译过程中将所有 Lambda 表达式转换成了匿名内部类。然而,事实并非如此简单。在 JVM 的层面上,Lambda 表达式与匿名内部类实际上存在着显著的差异。
5.1 匿名内部类的实现
匿名内部类本质上依然属于一个类范畴,但与常规类不同,我们无需手动为其命名,编译器会自动生成一个名称。以以下代码为例:
public class LambdaTest {
new Thread(new Runnable {
@Override
public void run {
输出:“您好,世界!”
}
}).start;
}
}
编译之后将会产生两个 class 文件:
LambdaTest.class
LambdaTest$1.class
运用 javap -c 命令对 LambdaTest.class 文件进行深入的字节码解析,其部分分析结果呈现如下:
public static void 执行主函数,参数为java.lang.String类型的数组;
Code:
在Java语言中,针对线程类,我们实施了严格的限制,确保#2编号的类不被修改。
3: dup
4: 新建编号为3的类,类名为com/example/my,禁止修改。application/lambda/LambdaTest$1
7: dup
调用了特定的编号为#4的方法,该方法是com/example/my包下的。app在文件lication/lambda/LambdaTest中,编号为1的Lambda测试实例被禁止修改。":V
调用特殊方法编号为#5,即java/lang/Thread类中的方法。":(Ljava/lang/Runnable;)V
禁止调用编号为6的方法,该方法对应的是`java/lang/Thread`类中的`start`方法,其作用是启动线程。
17: return
在代码的第4行,即“4: new #3”这一行,我们能够观察到匿名内部类的实例被成功创建。
5.2 Lambda 表达式的实现
接下来,我们将对上述示例代码进行Lambda表达式的转换,具体实现方式如下:,代码呈现如下:
public class LambdaTest {
public static void main(String[] args) {
启动一个新的线程,该线程执行打印"Hello World"的操作。
}
}
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Thread
3: dup
调用动态指令#3,参数为0,对应于InvokeDynamic #0:run:Ljava/lang/Runnable;
调用特定方法,编号为#4,即java/lang/Thread类中的方法。":(Ljava/lang/Runnable;)V
禁止调用该线程的启动方法。
15: return
观察上述结果可知,Lambda 表达式实际上被转化为主类中的一个私有方法,并且是通过 invokedynamic 指令来执行调用的。
由此,我们能够推断出:Lambda 表达式是借助 invokedynamic 指令来实现的,同时,编写 Lambda 表达式并不会导致新类的生成。
由于 Lambda 表达式并不生成匿名内部类,因此在 Lambda 表达式中使用 this 关键字时,它所指的是外部类的引用。
优缺点
优点:
缺点:
本网站每日更新互联网创业教程,一年会员只需98,全站资源免费下载点击查看会员权益