Java Lambda 符号引用::探析与小结
本文是基于JDK8.0下做的测试和研究
参考:https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-LambdaExpression
NPE问题探究:https://stackoverflow.com/questions/65101313/what-wrong-with-java-lambda-expression-why-what-diff-between-and-norma
查看字节码命令:javap -c -verbose xxx.class
符号引用的场景与方式
- 类签名实例方法引用
- 实例签名方法引用
- 类签名静态方法的引用
- 实例化方法(todo):对象的new、数组的new
- 数组方法(todo):clone
- 泛型在符号引用下的使用(todo)
具体示例
这样说有点抽象,我们来看个具体例子,假设有个如下类
public class Main {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public static String buildName(Integer param) {
return "@@@" + param + "@@@";
}
}
我们可以用如下方式引用Main
类的方法
public static void main(String[] args) {
Main main = new Main();
// 类签名实例方法引用
Function<Main, String> getNameFunction = Main::getName;
// 实例签名方法引用
Supplier<String> getNameSupplier = main::getName;
// 类签名实例方法引用
BiConsumer<Main, String> setNameBiConsumer = Main::setName;
// 实例签名方法引用
Consumer<String> setNameConsumer = main::setName;
// 类签名静态方法的引用
Function<Integer, String> buildName = Main::buildName;
}
类签名实例方法引用
Function<Main, String> getNameFunction = Main::getName;
BiConsumer<Main, String> setNameBiConsumer = Main::setName;
上面两种都属于【类签名实例方法引用】,只不过由于方法的签名(参数、返回值)不同,属于不用的函数接口类型,这些函数接口由于是常用的,在JDK里都有定义,在包java.util.function
下面,都标注为@FunctionalInterface
了
我们看下这两个函数接口的定义
@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);
}
@FunctionalInterface
public interface BiConsumer<T, U> {
/**
* Performs this operation on the given arguments.
*
* @param t the first input argument
* @param u the second input argument
*/
void accept(T t, U u);
}
只要定义的方法的签名和它们一样,就可以申明该类型为函数接口类型
我们知道调用类的实例方法必须要有类的实例才可以调用,所以这些方法的参数必须要有一个参数为该类的类型,这样JVM才可以帮我们调用,而且必须是第一个参数
为什么必须是第一个参数?
还记得大家在学JVM的字节码章程中的方法调用栈中的局部变量表Slot吗,第一个存的就是this对象,在由java文件编译为class字节码时,JVM已经隐含的帮我们把该方法引用的对象作为第一个参数传进来了,所以【类签名实例方法引用】第一个参数必然是该类的引用。在调用时,需要我们在代码中传进来。它看起来更多的像一个静态方法,而第一个参数是固定的。
我们可以这样使用他们
public static String useFunction(Main main, Function<Main, String> function) {
return Objects.nonNull(main) ? function.apply(main) : null;
}
public static void useBiConsumer(Main main, BiConsumer<Main, String> biConsumer) {
if (Objects.nonNull(main)) {
biConsumer.accept(main, "setName");
}
}
实例签名方法引用
Supplier<String> getNameSupplier = main::getName;
-
Consumer<String> setNameConsumer = main::setName;
除了【类签名实例方法引用】可以引用类的实例方法,还可以使用【实例签名方法引用】,这两个函数接口类型,我就不贴了,大家自己去包java.util.function
下面找下
本质上,下面的调用效果是一样的
Main main = new Main();
// 原始调用
String name = main.getName();
// 方式1-类签名实例方法引用
Function<Main, String> getNameFunction = Main::getName;
String name = getNameFunction.apply(main);
// 方式2-实例签名方法引用
Supplier<String> getNameSupplier = main::getName;
String name = getNameSupplier.get();
那有人就会问了,他俩有啥区别,啥时候该用方式1,啥时候该用方式2?我总结区别如下:
-
Main::getName
这种方式将类的方法的纯引用,上面也说了,它更像一个静态方法。把实例和方法分开了。因此我们可以将类的实例方法作为参数传递出去,让拥有实例的方法帮我们去调用,这样有时候代码可以写的非常简洁紧凑 - 而
main::getName
这种已经已经耦合了实例对象,作为参数调用的时候,只是一个调用,然后拿结果,更像一个成品。 - 此外【实例签名方法引用】在实例为
null
时,即使你的代码没有走到调用处也会触发 NPE 异常(JDK8.0版本及之前),据说这个问题在之后的高版本中得到修复,我还没有机会进行验证。从直觉上来看,这属于一个BUG,因为代码都没有执行。以下是测试代码
public static String useFunction(Main main, Function<Main, String> function) {
return Objects.nonNull(main) ? function.apply(main) : null;
}
public static String useSupplier(Main main, Supplier<String> supplier) {
return Objects.nonNull(main) ? supplier.get() : null;
}
public static void useBiConsumer(Main main, BiConsumer<Main, String> biConsumer) {
if (Objects.nonNull(main)) {
biConsumer.accept(main, "setName");
}
}
public static void useConsumer(Main main, Consumer<String> consumer) {
if (Objects.nonNull(main)) {
consumer.accept("setName");
}
}
public static void main(String[] args) {
// NPE
Main mainNull = null;
String fValue = Main.useFunction(mainNull, Main::getName);
System.out.println("fValue = " + fValue);
String sValue = Main.useSupplier(mainNull, mainNull::getName); // will throw NPE exception
System.out.println("sValue = " + sValue);
String sValue1 = Main.useSupplier(mainNull, () -> mainNull.getName());
System.out.println("sValue1 = " + sValue1);
Main.useBiConsumer(mainNull, Main::setName);
Main.useConsumer(mainNull, mainNull::setName); // // will throw NPE exception
Main.useConsumer(mainNull, (x) -> mainNull.setName(x));
}
如果一定要这样使用,最好的方式是用Main::getName
这种方式重构方法参数,如果不想修改,一个折中的方式是() -> mainNull.getName()
将该方法包装成一个新的lambda
表达式
类签名静态方法的引用
Function<Integer, String> buildName = Main::buildName;
这个没什么好说的,因为是静态方法,比较简单,参数与函数接口的参数是一致的
public static String buildName(Integer param) {
return "@@@" + param + "@@@";
}
public static void main(String[] args) {
// 普通调用方式
String name = Main.buildName("pppp");
// 类签名静态方法的引用
Function<Integer, String> buildName = Main::buildName;
String name = buildName.apply("pppp");
}
如果类的方法参数比较复杂怎么办
我们可以通过自定义函数接口来实现复杂方法的lambda
参数化,假设我们的Main
类有个如下方法
// BiFunction
public String getName(Integer param) {
return this.name + param;
}
// TiFunction
public String getName(Integer param, Object obj) {
return this.name + param + obj;
}
我们可以这样,定义一个函数接口
@FunctionalInterface
public interface TiFunction<T, U1, U2, R> {
R apply(T t, U1 u1, U2 u2);
}
然后就可以这样使用了
public static void main(String[] args) {
// 一个复杂的例子
BiFunction<Main, Integer, String> getNameBiFunction = Main::getName;
// 多个参数
TiFunction<Main, Integer, Object, String> getNameTiFunction = Main::getName;
}