java8学习笔记(1)-Lambda表达式

2019/06/11

初识 Lambda 表达式

何谓 Lambda 表达式

Lambda 表达式是一种用于指定匿名函数(anonymous function)或闭包(closure)的运算符。可以简单地将其理解为一种传递行为的表达式,常于函数式编程(面向行为)。

使用 Lambda 表达式的好处

在 Java7 中,我们无法将函数作为参数传递给一个方法,也无法声明返回一个函数的方法,也就是说,我们无法把过程作为对象来使用。

而在 JavaScript 和 Python 这样的语言中,把函数传递给另一个函数,或在一个函数中返回另一个函数,都是很常见的(尤其是钩子和回调)。甚至在 C/C++ 乃至汇编中,我们也可以通过传递函数地址来实现类似的操作。

比如,在下列 JavaScript 代码中,我们可以把isOdd函数作为一个参数传入到print函数中:

// 判断是否为偶数
function isOdd(n) {
    return n % 2 === 0;
}

// 基于传入的策略函数打印数组中的部分元素
function print(arr, strategy) {
    for (let i = 0; i < arr.length; i++) {
        if (strategy(arr[i])) {
            console.log(arr[i]);
        }
    }
}

print([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], isOdd);

运行结果是:

0
2
4
6
8

在 C/C++ 中,通过函数指针也可以实现同样的需求,以 C++ 为例:

#include <iostream>

using std::cout;
using std::endl;

bool isOdd(int n) {
    return (n % 2) == 0;
}

void print(int arr[], int length, bool (*strategy)(int)) {
    for (int i = 0; i < length; i++) {
        if (strategy(arr[i])) {
            cout << arr[i] << endl;
        }
    }
}

int main() {
    int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    print(arr, 10, isOdd);
    return 0;
}

运行结果同样是:

0
2
4
6
8

在上面两个例子中,我们可以在调用print函数的时候动态地通过钩子函数传入过滤策略,实现对打印过程的复用。

在 JAVA8 之前,我们要做到类似的事情,多是通过接口、抽象类或是模板方法模式之类的方式,相比之下代码量大得多。

比如这样写一个接口:

public interface Strategy {
    boolean doFilt(int n);
}

然后在调用print方法时,创建一个Strategy类并实现doFilt(int n)方法,再将它的对象作为参数传入,然后通过调用这个对象的方法来应用具体的过滤策略:

public class Main {
    public static void print(int[] arr, Strategy strategy) {
        for (int i : arr) {
            if (strategy.doFilt(i)) {
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        print(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, new Strategy() {
            @Override
            public boolean doFilt(int n) {
                return n % 2 == 0;
            }
        });
    }
}

按照这种写法,又或者是使用模板方法模式,对于每一种过滤策略,我们都必须新创建一个类(经常是匿名内部类)。这对于ptint方法的调用者而言,实现代码相对臃肿,可读性相对较低,写起来也不够方便。

而使用 Lambda 表达式,只需要这样写:

public static void main(String[] args) {
    print(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, n -> n % 2 == 0);
}

在这种写法中,我们只需要传入n -> n % 2 == 0,而不是写一整个实现了Strategy接口的类,为print方法的调用者带来了很多便利。

Lambda 表达式的使用

使用条件

Java 中 Lambda 表达式可用于替代函数式接口的新建匿名实现类对象。

所谓函数式接口,指的是有且仅有一个抽象方法(区别于 java8 新增的默认方法)的接口。值得注意的是,根据@FunctionalInterface的文档,如果一个接口中定义了覆写java.lang.Object的公有方法的方法,这个覆写的方法不属于抽象方法,因为任何一个该接口的实现类必然也是java.lang.Object的子类。

语法格式

Lambda 表达式的语法结构为(参数列表) -> {函数体}

比如,对于下面这段代码:

public class Main {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7);
        list.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });
    }
}

这里的new Consumer<>(){}就可以用 Lambda 表达式替换成如下代码:

(Integer integer) -> {
    System.out.println(integer);
}

无论有多少个参数,Lambda 表达式的参数列表都是可以省略类型的,毕竟函数式接口只有一个抽象方法,不会产生歧义。比如上述 Lambda 表达式可以省略Integer,即改成这样:

(integer) -> {
    System.out.println(integer);
}

对于抽象方法只传入一个参数的函数式接口,我们可以省略圆括号;而如果函数体只有一条语句,我们可以省略花括号、分号和关键字return。也就是说,上面的 Lambda 表达式可以写成这样:

integer -> System.out.println(integer)

实际上,这里还可以换成更简洁的方法引用:

public static void main(String[] args) {
    List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7);
    list.forEach(System.out::println);
}

关于方法引用,以后再深究。

Lambda 表达式是对象

在 Java 中,Lambda 表达式是以对象的形式存在的,它实际上是一个函数式接口的对象。既然是对象,就会有类型,而 Lambda 表达式的语法结构中并没有关于表达式自身类型的信息,Lambda 表达式的类型是编译器根据上下文进行推断的。

运行下面的代码:

public class Main {

    interface ExampleInterfaceA {
        void exampleMethod();
    }

    interface ExampleInterfaceB {
        void exampleMethod();
    }

    public static void main(String[] args) {
        ExampleInterfaceA a = ()->{};
        ExampleInterfaceB b = ()->{};

        System.out.println(a.getClass());
        System.out.println(a.getClass().getInterfaces()[0]);
        System.out.println(b.getClass());
        System.out.println(b.getClass().getInterfaces()[0]);
    }
}

运行结果:

class Main$$Lambda$1/205797316
interface Main$ExampleInterfaceA
class Main$$Lambda$2/935044096
interface Main$ExampleInterfaceB

根据运行结果,ab的类型都是匿名类,而所实现的接口显然与声明的接口相同。

如果上下文中没有函数式接口类型信息,比如写成Object o = ()->{};,是无法通过编译的。如下图所示:

必须给 Lambda 表达式指定函数式接口类型


(转载本站文章请注明作者和出处)

Show Disqus Comments

Post Directory