Java8 Lambda、函数式接口用法及原理
Java 15 的试验版已经悄悄发布了,但是在 Java 界一直有个神秘现象,那就是「你发你发任你发,我的最爱 Java 8」。
尽管 Java 8 发布多年,使用者众多,可神奇的是竟然有很多同学没有用过 Java 8 的新特性,比如 Lambda表达式、比如方法引用。不少使用Python、Scala的开发者,对Lambda表达式的使用可谓直呼流畅啊,现在写Java也可以很流畅啦。
Lambda 表达式
Lambda 表达式是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象,是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包。
在 Java 中,Lambda 表达式的格式是像下面这样
// 无参数,无返回值 () -> log.info("Lambda") // 有参数,有返回值 (int a, int b) -> { a+b }
其等价于
log.info("Lambda"); private int plus(int a, int b){ return a+b; }
最常见的一个例子就是新建线程,有时候为了省事,会用下面的方法创建并启动一个线程,这是匿名内部类的写法,new Thread需要一个 implements 自Runnable类型的对象实例作为参数,比较好的方式是创建一个新类,这个类 implements Runnable,然后 new 出这个新类的实例作为参数传给 Thread。而匿名内部类不用找对象接收,直接当做参数。
new Thread(new Runnable() { @Override public void run() { System.out.println("快速新建并启动一个线程"); }}).start();
但是这样写是不是感觉看上去很乱、很土,而这时候,换上 Lambda 表达式就是另外一种感觉了。
new Thread(()->{ System.out.println("快速新建并启动一个线程"); }).start();
怎么样,这样一改,瞬间感觉清新脱俗了不少,简洁优雅了不少。
Lambda 表达式简化了匿名内部类的形式,可以达到同样的效果,但是 Lambda 要优雅的多。虽然最终达到的目的是一样的,但其实内部的实现原理却不相同。
匿名内部类在编译之后会创建一个新的匿名内部类出来,而 Lambda 是调用 JVM invokedynamic指令实现的,并不会产生新类。
方法引用
方法引用的出现,使得我们可以将一个方法赋给一个变量或者作为参数传递给另外一个方法。::双冒号作为方法引用的符号,比如下面这两行语句,引用 Integer类的 parseInt方法。
Function<String, Integer> s = Integer::parseInt; Integer i = s.apply("10");
或者下面这两行,引用 Integer类的 compare方法。
Comparator<Integer> comparator = Integer::compare; int result = comparator.compare(100,10);
再比如,下面这两行代码,同样是引用 Integer类的 compare方法,但是返回类型却不一样,但却都能正常执行,并正确返回。
IntBinaryOperator intBinaryOperator = Integer::compare; int result = intBinaryOperator.applyAsInt(10,100);
相信有的同学看到这里恐怕是下面这个状态,完全不可理喻吗,也太随便了吧,返回给谁都能接盘。
先别激动,来来来,现在咱们就来解惑,解除蒙圈脸。
#####Q:什么样的方法可以被引用?
A:这么说吧,任何你有办法访问到的方法都可以被引用。
#####Q:返回值到底是什么类型?
A:这就问到点儿上了,上面又是 Function、又是Comparator、又是 IntBinaryOperator的,看上去好像没有规律,其实不然。
返回的类型是 Java 8 专门定义的函数式接口,这类接口用 @FunctionalInterface 注解。
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
还有很关键的一点,你的引用方法的参数个数、类型,返回值类型要和函数式接口中的方法声明一一对应才行。比如 Integer.parseInt方法定义如下:
public static int parseInt(String s) throws NumberFormatException { return parseInt(s,10); }
首先parseInt方法的参数个数是 1 个,而 Function中的 apply方法参数个数也是 1 个,参数个数对应上了,再来,apply方法的参数类型和返回类型是泛型类型,所以肯定能和 parseInt方法对应上。
这样一来,就可以正确的接收Integer::parseInt的方法引用,并可以调用Funciton的apply方法,这时候,调用到的其实就是对应的 Integer.parseInt方法了。
用这套标准套到 Integer::compare方法上,就不难理解为什么即可以用 Comparator<integer>接收,又可以用 IntBinaryOperator接收了,而且调用它们各自的方法都能正确的返回结果。</integer>
public static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); }
返回值类型 int,两个参数,并且参数类型都是 int。然后来看Comparator和IntBinaryOperator它们两个的函数式接口定义和其中对应的方法:
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); }@FunctionalInterface public interface IntBinaryOperator { int applyAsInt(int left, int right); }
对不对,都能正确的匹配上,所以前面示例中用这两个函数式接口都能正常接收。其实不止这两个,只要是在某个函数式接口中声明了这样的方法:两个参数,参数类型是 int或者泛型,并且返回值是 int或者泛型的,都可以完美接收。
自己动手实现一个例子
1. 定义一个函数式接口,并添加一个方法
定义了名称为 KiteFunction 的函数式接口,使用 @FunctionalInterface注解,然后声明了具有两个参数的方法 run,都是泛型类型,返回结果也是泛型。
还有一点很重要,函数式接口中只能声明一个可被实现的方法,你不能声明了一个 run方法,又声明一个 start方法,到时候编译器就不知道用哪个接收了。而用default 关键字修饰的方法则没有影响。
@FunctionalInterface public interface KiteFunction<T, R, S> { /** * 定义一个双参数的方法 * @param t * @param s * @return */ R run(T t,S s); }
2. 定义一个与 KiteFunction 中 run 方法对应的方法
在 FunctionTest 类中定义了方法 DateFormat,一个将 LocalDateTime类型格式化为字符串类型的方法。
public class FunctionTest { public static String DateFormat(LocalDateTime dateTime, String partten) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten); return dateTime.format(dateTimeFormatter); }}
3.用方法引用的方式调用
正常情况下我们直接使用 FunctionTest.DateFormat()就可以了。而用函数式方式,是这样的。
KiteFunction<LocalDateTime,String,String> functionDateFormat = FunctionTest::DateFormat; String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");
而其实我可以不专门在外面定义 DateFormat这个方法,而是像下面这样,使用匿名内部类。
public static void main(String[] args) throws Exception { String dateString = new KiteFunction<LocalDateTime, String, String>() { @Override public String run(LocalDateTime localDateTime, String s) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s); return localDateTime.format(dateTimeFormatter); } }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss"); System.out.println(dateString);}
前面第一个 Runnable的例子也提到了,这样的匿名内部类可以用 Lambda 表达式的形式简写,简写后的代码如下:
public static void main(String[] args) throws Exception { KiteFunction<LocalDateTime, String, String> functionDateFormat = (LocalDateTime dateTime, String partten) -> { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten); return dateTime.format(dateTimeFormatter); }; String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss"); System.out.println(dateString);}
使用(LocalDateTime dateTime, String partten) -> { } 这样的 Lambda 表达式直接返回方法引用。
Q:方法引用符号::的写法和lambda表达式 ()->{}的写法有什么不一样?
A:其实是一样的。从作为参数的效果来看,都是相当于一个函数接口的实现类。可以认为方法引用符号::的写法是lambda表达式的一种简化写法。
关于作者
作者就职于一线互联网公司负责离线、实时数据开发,每天支持处理千亿级别数据。坚持分享数仓理论、大数据开发技术干货,同时欢迎交流,关注公众号 "大数据开发指南",回复:“联系作者”,添加你身边那位懂数据的朋友。