Posted on 周二 14 六月 2022

概述

从java8开始,java引入了lambda表达式。网络上围绕lambda表达式有很多文章,但大部分都是描述如何使用,但lambda表达式的本质是什么, 很少有文章能说清楚。

本文主要通过一个实例来说明java的lambda表达式的使用,以及java的lambda表达式的内在本质,期望能帮助大家理解和使用lambda表达式。

本文的示例代码都在git中,本文代码

也可以直接下载整个项目

git clone http://git.gzjunbo.net/pl/lambda_demo.git

什么是lambda表达式

为什么会这样?

大家可以看下代码中单元测试类PersonCompareTest的testCompare的代码

单元测试类PersonCompareTest的testCompare的代码

可能有部分同学不明白这是什么写法,这段代码到底什么意思?那我们就解读下这段代码的故事。

我们在

原始方法的原始定义

按住ctrl点击鼠标,找到原始方法的原始定义,可以看到这个方法实际调用的是

方法调用

我们也已经知道,Java是强类型语言,调用方法传递的参数类型必须与方法定义的参数类型相同。

我们仔细检查下调用者和被调用方法的参数列表:person1是Person类的对象,person2也是Person类的对象,前2个参数完美符合。

但我们传递给第三个参数的代码是这样写的:

传递给第三个参数的代码

编译器竟然认为它是PersonComparor类的对象,为什么会这样

Java的lambda表达式

这就是Java的lambda表达式。lambda的表达式的完整定义我就不写了,这是一个链接**Lambda表达式-百度百科 **

lambda表达式是一个数学概念,或者说所有编程语言都可以有的概念。

Java8之前,Java并不支持lambda表达式。因此上面例子的代码使用低于ava8的javac是编译通不过的。

而上面的第三个参数的代码就是Java的lambda表达式。

lambda表达式的基本写法:

(参数1,参数2 ...)->{
    //具体代码
   // return 根据情况是否需要返回
}

在Java中,每一个lambda表达式会编译成一个匿名的内部类,而这个类实现了参数定义的接口,例如上面的例子

传递给第三个参数的代码

它调用的方法对应的参数类是PersonComparor接口,因此,编译器大致会将它转换为下面的代码:

编译转换的代码示例

注意: 编译器只会编译为字节码,不会转换为上述Java代码,但字节码“转换”回java代码大致会像上面这样。

因此实际上我们传递给方法的是一个PersonComparor的实例,代码也就能顺利运行了。

那是不是一个方法的参数是任意的类和接口都可以传递lambda表达式给他呢?答案是否定的,但为什么?这里卖个关子,下面再说。

lambda表达式的好处

从前面的代码我们可以看到,使用lambda表达式实际上是让编译器帮我们完成了自定义内部匿名类的工作。

lambda表达式让开发者可以简单明了的调用自定义方法,而不用像传统做法一样定义接口然后实现,或者定义接口然后使用匿名内部类来实现, 使用lambda表达式的代码明显简化,且可读性更强。

一个常见的使用场景

我们经常面临的一个场景,项目中有一个Person类,下面是简化的Person类

public class Person {
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private final String name;
    private final int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

为了排序或者其他需要,我们需要实现对不同Person的比较,出于排序场景的不同,我们可能需要根据姓名、年龄等等不同的比较规则, 并且我们可能需要根据运行时的情况来灵活选择不同的比较方式

Java8之前的传统做法

为了完成上述场景的需要,因此我们定义了一个对人进行比较的接口:

/**
 * 对2个人进行比较的接口,演示lambda表达式
 *
 * @author panli
 */
@FunctionalInterface
public interface PersonComparor {
    /**
     * 对2个人进行比较
     *
     * @param person1 参与比较的人1
     * @param person2 参与比较的人2
     * @return 比较的结果,大于0 为 person1> person2, 等于0 为  person1 = person2, 小于0 为 person1 < person2
     */
    int compare(Person person1, Person person2);
}

在需要进行不同的比较时,我们可以实现这个接口,例如

public class PersonAgeComparor  implements PersonComparor {
    @Override
    public int compare(Person person1, Person person2) {
        return PersonUtil.compareByAge(person1,person2);
    }
}

然后我们可以通过匿名内部类实现接口来调用,或者用已定义好的类来调用。

在单元测试中,可以看到用这种方式的代码

单元测试示例代码

这里面分别用了匿名内部类和使用预先定义好的类的方式。

lambda表达式的方式

传统方式中,需要实现扩展不同的方式,要么不断的定义新的实现类,要么使用匿名内部类,可读性极差。并且设想下,假设我们已经定义了一些比较方法,例如

public class PersonUtil {  
    public static int compareByAge(Person person1, Person person2) {
        return person1.getAge() - person2.getAge();
    }

    public static int compareByName(Person person1, Person person2) {
        return person1.getName().compareTo(person2.getName());
    }

    public static int compareByAge(Person person1, Person person2, boolean isAsc) {
        if (isAsc) {
            return person1.getAge() - person2.getAge();
        } else {
            return person2.getAge() - person1.getAge();
        }
    }
}

但由于这些是 方法 不是 ,所以没有办法将它作为一个参数传递给其他方法,需要的时候可能只能再加一层类的封装, 如同我们在PersonAgeComparor所作的一样。

为了给开发者提供方便,Java8开始提供了lambda表达式。

lambda表达式

在Java中,lambda表达式实际上是一个语法糖,它对java的本质并没有改变,在java中lambda表达式会编译成一个匿名内部类, 但是他为开发者提供了很大的便利。

注意: 如果对Java的泛型熟悉的话,也可以知道泛型也是一个的语法糖,它并没有改变Java的本质,所以才会有 泛型擦除 的问题, 这也是一个语言为了兼容所做的无奈妥协。

lambda表达式的基本写法

可以看看我们的测试代码:

测试代码

在文章开头我们就已经提到过,它实际上是定义了一个PersonComparor的匿名内部类,这里面会带来一个疑问,编译器会怎么检查参数类型和返回值类型呢?

函数式接口

为了支持lambda表达式,Java8新增了 函数式接口 的概念, 如果一个接口仅定义了一个方法,编译器就会认为这是一个函数式接口,这个接口就可以作为lambda表达式的类型

例如PersonComparor就是一个函数式接口,它就可以作为一个lambda表达式的类型,编译器是 根据接口的签名(参数和返回值类型)来对lambda表达式的参数和返回值类型 进行检查的。

注意: 编译器在编译lambda表达式的时候,实际上是将lambda表达式转化为一个匿名内部类,这也是我们说lambda表达式是一个语法糖的原因。

为了支持函数式接口,Java8新增了一个注解 FunctionalInterface ,这个注解仅用于接口,并且只能用于仅定义了一个方法的接口,如果一个接口内部定义了超过一个方法,然后又标记为函数式接口,编译就会报错。

但是 只要一个接口只定义了一个方法,编译器就会认为这是个函数式接口,并不依赖于FunctionalInterface注解

tips: 可能有人会问,这个注解存在的意义是什么呢?
这样注解的目的是防止自己或者其他人以后在这个接口中新增方法:
如果没有增加注解,接口中新增了方法后,编译器不会发现错误,但是接口就不能作为lambda表达式使用了, 这可能会给将这个接口作为lambda表达式使用的代码带来灾难性的后果;
而如果增加了这个注解,接口中新增了方法后,编译器马上就会报错,避免出现问题。
因此我们建议 如果定义了一个接口准备作为函数式接口的话,都应添加上FunctionalInterface注解

tips: 特别需要注意的是 一个方法
在java8之前,接口中仅支持定义方法,不允许有方法的实现和变量定义。
但是从java8之后,接口中可以允许方法的实现,但是这个实现必须加上 default 关键字。
函数式接口的“一个方法”是不包含含有default声明的方法的,因此我们可以看到Java8自定义的函数式接口中有可能包含多个方法, 但需要外部实现的方法只会有一个。

lambda表达式的其他写法

除了基本写法之外,java8还提供了lambda表达式的其他简略写法。

仅一行实现代码的写法

仅一行实现代码的写法

如果实现的代码只有一行,直接返回结果,那可以省略{ } 和 return ,见上图的写法

签名相同的写法

签名相同的写法

如果一个方法与函数式式接口的方法签名是兼容的,则可以用java8新定义的运算符 “ ::”来传递lambda表达式

可以看到上面例子的签名是相同的。

签名1

签名2

tips: 对于使用函数式接口作为参数之一的方法,任意一个与接口签名相同的方法都可以作为参数进行传递。

参数签名不同的写法

有时候我们会面临一些问题,比如我们想要利用的方法与被调用的接口签名不同,但我就是想调用,怎么办?

例如PersonComparor的签名是这样的:

PersonComparor的签名

但是我们想利用的方法的签名是这样的:

调用签名

我们可以确定现在就是要用升序比较,因此我们定义一个lambda表达式就可以完美利用了

调用

形参(p1,p2)与PersonComparo签名一样,但调用时我们根据需要选择是否使用。

tips: 其实这个写法就是仅一行实现代码的写法

Java的函数式编程

有了lambda表达式,java.util.function中定义了多个接口供开发者使用

java.util.function定义的接口

对于这些接口,大家可以理解为是一堆预定义的函数式接口,每个有特定用途。

例子中的PersonComparor其实就是BiFunction的一个特例。PersonUtilWithJavaFunctionalInterface.compare() 是利用了java.util.function定义的BiFunction实现的,而PersonUtil.compare()是使用自定义的PersonComparor实现的,二者实际上是等价的。

tips: 在实际工作中,可以不用定义PersonComparor接口,直接使用BiFunction实现。
java.util.function中的接口都是通过泛型定义的,具有较好的通用性。实际使用时,将泛型类参数替换为自己需要的类型即可。

因此我们可以看到单元测试中的2种写法的对比

2种写法的对比

而对于BiFunction,我们也一样可以用传统的匿名内部类的方式来实现

匿名内部类实现

匿名内部类实现2

大家可以看下2个测试中的具体例子,其他代码我就不贴了。

总结

Java中的lambda表达式是Java8开始提供的新特性,它为代码简化提供了一个方式。

当一个方法的参数类型符合函数式接口的要求时,我们就可以使用lambda表达式给他传参。Java8 的编译器会帮助我们将这个lambda表达式翻译成内部匿名类,我们不用显式去写。

java.util.funtion使用泛型封装了大部分我们可能用到的函数式接口,根据 参数个数、是否需要返回值、返回值类型 选用就可以了,大部分情况下不需要自定义。

入参个数 需要返回值 返回值类型 接口
1 自定义 Function
1 boolean Predicate
2 自定义 BiFunction
0 自定义 Supplier
1 自定义 Consumer
2 自定义 BiConsumer

上表列出了常用的一些接口选择,大家可以直接点开java.util.funtion的包,看每个接口的定义选用

Links