Theme Preview

Hue:

You are using an outdated browser that does not support OKLCH colors. The color setting will not take effect.

Java语言下的函数指针

1346 字

首先要说明的是在Java中并不存在指针概念,也没有什么所谓的黑科技。而得益于 Lambda 表达式在 Java8 中的引入,使得我们可以模仿函数指针来实现一套相似的逻辑。因此本文在介绍 Java 版本的函数指针时要从 Lambda 表达式开始。

一、Lambda 是什么

Lambda 表达式是一个匿名函数,可以将代码像数据一样进行传递。换句话说 Lambda 表达式是函数式编程的一种体现,它允许将函数当作参数传递给方法,或者将函数作为返回值,这种支持使得 Java 在函数式编程方面更为灵活,能够更好地处理集合操作、并行计算等任务。

1.1 代码简化过程

Java 作为一个面向对象的语言,通常会创建一系列的类来封装和处理具体的业务逻辑。在面对需要创建临时对象或者实现接口的情况下,“匿名内部类”的概念就会使得代码十分简洁。比方说我们使用如下内部类来比较两个数据的大小

TreeSet<Integer> treeSet = new TreeSet<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
});
treeSet.add(2);
treeSet.add(1);
treeSet.add(5);
System.out.println(treeSet.toString());

这其中真正有效的语句是 Integer.compare(o1, o2) , 但匿名内部类的写法还是让我们写了很多冗余的代码,当比较的规则比较多的时候,可能就显比较杂乱。因此在此基础上,可以进一步采用设计模式,比如 策略模式 来改进,不过这同样带了一个问题:每种比较规则都会创建一个新的类,这是代码清晰要付出的代价。

那么当我们使用 Lambda 表达式会怎样呢?

TreeSet<Integer> treeSet = new TreeSet<>((o1, o2) -> Integer.compare(o1, o2));
treeSet.add(2);
treeSet.add(1);
treeSet.add(5);
System.out.println(treeSet.toString());

看到了吗!原来相对复杂的代码精简到了最核心的一行,几乎去掉了所有冗余的代码!

1.2 Lambda 的原理分析

Lambda 表达式可以极大的简化我们的代码,但不是在任何地方都可以直接使用 Lambda 表达式,这需要两个依据:

  • 必须有相应的函数接口
  • 类型推断机制

怎么理解呢?函数接口是指内部只有一个抽象方法的接口,实际上 Lambda 的类型就是对应函数接口的类型就(类似 “张三” 在程序中对应的 String 类型),在上下文信息足够的情况下,编译器可以推断出参数表的类型,而不需要显式指明。

Lambda 表达式在 JVM 层级是通过 invokedynamic 指令实现,使用 Lambda 表达式不会产生新的类,这和匿名内部类不同。匿名内部类虽然在使用的时候不需要显示指定类名,但是编译器会自动为该类取名。以如下文件为例:

public class LambdaTest {
public static void main(String[] args) {
new Thread(
() -> System.out.println("Lambda Thread run()")
).start();;
}
}

使用 javac LambdaTest.java 得到 LambdaTest.class 文件,再使用 javap 反编译看那一下内容:

//javap -c -p LambdaTest.class
Compiled from "LambdaTest.java"
public class LambdaTest {
public LambdaTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #7 // class java/lang/Thread
3: dup
4: invokedynamic #9, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #13 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #16 // Method java/lang/Thread.start:()V
15: return

private static void lambda$main$0();
Code:
0: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #25 // String Lambda Thread run()
5: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}

反编译之后我们发现 Lambda 表达式被封装成了主类的一个 私有方法,并通过 invokedynamic 指令进行调用。

二、函数指针实现方式

经过以上对 Lambda 表达式的说明,接下来就根据表达式的特性来实现 Java 版本的 “函数指针”。

  1. 定义函数指针
public interface FunConsumer<U> {
void accept(U t);
}
  1. 函数实现
private void test1(String name){
System.out.println("test1 : "+name);
}

private void test2(String name){
System.out.println("test2 : "+name);
}

private void test3(String name){
System.out.println("test3 : "+name);
}
  1. 模拟调用
//指针数组
FunConsumer<String>[] funConsumers = new FunConsumer[3];
//这里之所以可以直接赋值,是因为编译器对 Lambda 表达式的类型推断
funConsumers[0] = this::test1;
funConsumers[1] = this::test2;
funConsumers[2] = this::test3;

//调用
funConsumers[0].accept("测试1");
funConsumers[1].accept("测试2");
funConsumers[2].accept("测试3");
  1. 结果
test1 : 测试1
test2 : 测试2
test3 : 测试3

关于上面的 this 说明,this 在 Java 中是代表对象本身,在这里是用来表示要引用的是对象的实例方法,在 Lambda 表达式中一共有四种引用方式:

方式 示例
引用静态方法 DemoClass::staticMethodName
引用特定对象的实例方法 demoClass::instanceMethodName
引用特定类型的任意对象的实例方法 String::compareToIgnoreCase
引用构造函数 HashSet::new

三、参考

//