出品 | 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,全站资源免费下载点击查看会员权益

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注