写点什么

Java lambda 表达式人类使用指南

用户头像
ES_her0
关注
发布于: 2021 年 02 月 23 日

Java 在 JDK1.8 之后增加一个新鲜物种,叫做 lambda 表达式,实际的写法是一个小箭头:->。它是什么东西呢?我们可以很容易的在各大技术社区查到,这东西叫函数。那函数又是什么东西?这里不得不吐槽一下最开始翻译英文文档的那批人,给后世引入的名词让很多人摸不着头脑,什么套接字,句柄等等。函数的英文是 Function,最早是由清朝的数学家李善兰翻译,他在他的著作《代数学》解释说:

凡此变数中函彼变数者,则此为彼之函数

意思就是一个量随着另一个量的变化而变化。其实理解起来并不困难,但把这东西突然说给写了很多年的面向对象的 Java 的人,似乎确实有点懵。尽管函数式编程在很早之前就广泛应用了,但在很多 Java 开发者心里,函数似乎跟 Java 从来不沾边。

这里不再做过多的历史渊源与好坏的讨论,函数式和面向对象各有优劣,怎么用全看个人。Java 在进化,提供了函数式的机制,这里来分享一些使用心得,看完至少可以保证能用的明明白白。


追根溯源

匿名函数


首先不要神化所谓的 lambda 表达式,它还有个名字叫匿名函数,不要被这个名字吓到。函数就可以理解为方法,一个方法得有方法名、参数、返回值类型、return 语句,匿名函数就只要一个非常简单的声明就可以实现需要的功能,比如计算两数之和(以 Java 举例):

(a, b) -> a + b
复制代码

上古时代的程序员们使用它的理由是,很多时候只是一个小功能不会再复用的(肯定不是两数之和,这里可能特定场景的业务功能),没必要完整的去声明一个函数。它没有实际的方法论,更多的是一个编程的风格。匿名函数这个特性最早是 1958 年就被加入到 LISP 中,后面诞生很多语言都借鉴了这个特性,LISP 永远滴神!

Java lambda 表达式的几种写法

各个语言对于 lambda 的写法五花八门,这里列举 Java 的几种写法。

  • 无参数,且返回值为 void

Runnable run = () -> System.out.println("Hello, world.")
复制代码


  • 只包含一个参数,可以参数的省略括号

int res = param -> param + 1
复制代码


  • 包含多个参数的,箭头后为一个表达式

(a, b) -> a + b
复制代码


  • 包含多个参数,但有明确的类型

(long id, String name) -> "id: " + id + ", name:" + name
复制代码


  • 包含代码块

(a, b) -> { return a + b; }
复制代码


  • 方法引用

IntBinaryOperator sum = Integer::sum;
复制代码

以上所有的参数类型都是由 Java 的编译器推断出来的,当然也可以指定明确的类型。对于方法引用这种写法也一样,根据函数式接口的类型在指定的类中自动匹配参数类型和返回类型一致的方法。可以认为这是 Java 拥抱函数式的第一步,使用了匿名函数的外形,让代码看起来更加整洁精炼。但这个->的操作符不是凭空而来,它的背后是一个个的函数式接口。

函数式接口

函数式接口其实也没什么唬人的地方,首先看一下 oracle 官方的定义:

Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract.

However, the compiler will treat any interface meeting the definition of a functional interface as a functional interface regardless of whether or not a FunctionalInterface annotation is present on the interface declaration.

说的意思就是函数式接口只有一个抽象方法的接口。Java 8 引入了接口的默认方法机制,就是接口中的方法标记了 default 就可以直接在接口中写实现。接口中未实现的方法都是抽象方法,abstract 关键字可以省略。有且只有一个抽象方法的接口就可以称之为函数式接口。为了更明确的声明,jdk 提供了一个注解:FunctionalInterface。这个注解只是个标记,没有其他额外的处理和含义。如果你不用@FunctionalInterface标记但是符合定义的描述,编译器依然会认为这是个函数式接口,都可以创建 lambada 表达式,方法引用。

@FunctionalInterfaceinterface Square {     int calculate(int x); } 
复制代码

这是一个计算的数值的函数式接口,输入一个参数,返回计算完成的值,很简单。下面我们来使用它:

public static void main(String args[]) {   			// 定义这个函数        Square s = (int x)->x*x; // 只有一个参数,也可以写成 x -> x*x  			int a = 5;   			// 使用这个函数        int ans = s.calculate(a);         System.out.println(ans);  }
复制代码

我们没有给 Square 指定范型,因为这里编译器可以类型推断出这个方法会返回 int。下面我换一种写法,你一定非常熟悉:

public static void main(String args[]) {   			// 匿名内部类实现calculate方法        Square s = new Square() {      			@Override      			public int calculate(int x) {        			return x*x;      			}    		};  			int a = 5;   			// 使用这个接口        int ans = s.calculate(a);         System.out.println(ans);  }
复制代码

是不是一下子就没有秘密了?是的。上下两个写法几乎是等价的。把原来冗长的匿名内部类方法实现改成一行的‘函数’,结合编译器的类型推断,可以让代码看起来高大上一些。如果感觉 calculate 我需要更复杂一些的实现,也是可以的,稍微复杂点的逻辑就得用括号括起来,并且需要一个 return 语句,如下:

Square s = x -> {  int a = 3;  x = a + x;  // something complicate  return x * x;}
复制代码

另一个常见的例子是这个:

@FunctionalInterfacepublic interface Runnable {    public abstract void run();}
复制代码

你几乎可以在任何地方使用它,就像这样:

() -> System.out.println("Hello, world.");
复制代码

这个 run 方法没有参数,返回值也是 void。任何返回 void 的方法都可以这么执行,但你总这样无意义的用是不是就有点缺心眼。😄

总结起来,只要符合函数式接口的定义,并且参数和返回值都符合抽象方法的定义,就可以用这个->表示这个抽象方法的实现。有了这个认知的基础,我们就可以任意发挥了,其实官方早就定义好了一些预置的函数式接口,能覆盖大多数场景。

使用官方 API

下面简单介绍一下官方的几个接口和使用场景。

Function

@FunctionalInterfacepublic interface Function<T, R> {    R apply(T t); }
复制代码

输入一个参数 T,返回另一个对象 R。标准的输入输出,用途广泛,所以叫 function。

Consumer

@FunctionalInterfacepublic interface Consumer<T> {    void accept(T t);}
复制代码

输入一个参数 T,返回 void。消费资源,类似于 mq 消费消息,你用给 mq 返回任何东西。

Supplier

@FunctionalInterfacepublic interface Supplier<T> {    T get();}
复制代码

无参数,返回一个对象 T。索取资源,无需参数,每调用一次即可获取某个资源。

Predicate

@FunctionalInterfacepublic interface Predicate<T> {    boolean test(T t);}
复制代码

输入一个参数 T,返回一个布尔。俗称断言,输入一个值,判断对错。

以上几个官方定义的函数式接口看着名字挺唬人的,第一看看过去以为又有什么高深的实现要去记去背,其实没有,就是一些接口定义,只不过在特定场合给起了特定的名字。只要入参和返回值符合定义,就可以随便实现,随便用,就是这么简单。其实以上几个接口在 Stream 的使用非常广泛,不过大多数人可能先记住了一些常用写法,但没有深究为什么可以这么写。

值得说明的一点是,函数式接口可以用来实现惰性求值。传统的传值会立刻计算出所有参数的值,但如果把函数式接口当参数传递,在调用实现方法之前是不会求值的。举个例子:

public static String getName() {    System.out.print("method called\n");    return "called";  }
public static void main(String[] args) { String name1 = Optional.of("String").orElse(getName()); //output: method called String name2 = Optional.of("String").orElseGet(()->getName());// output: }
... public T orElse(T other) { return value != null ? value : other; }
public T orElseGet(Supplier<? extends T> other) { return value != null ? value : other.get(); }
复制代码

在 Optional 中使用 orElse 时,无论前面的是否为空都会执行参数。而 orElseGet 接收一个 Supplier,当 value 不为空的时候是不会执行 other.get()的,也就是不会求值,相对性能要好一些。

总结

Java 8 发布时至今日已有 8 年多了,很多人在写 Java 的过程中可能对于这个->的使用并不多,更多可能也是来自 IDE 的写法转换建议。初学或者非初学往往会被函数、lambda 这样的词汇“心生畏惧”,感觉是个特别高级的理论,然后一直半懂不懂。希望通过这篇文章让更多的人明白,lambda只是个名词罢了,包装的背后还是 Java 烂熟的那套机制。

发布于: 2021 年 02 月 23 日阅读数: 26
用户头像

ES_her0

关注

还未添加个人签名 2018.03.21 加入

还未添加个人简介

评论

发布
暂无评论
Java lambda表达式人类使用指南