如何理解 Java8 的函数式编程

lanhy
发布于 2020-9-1 10:30
浏览
0收藏

Java8 出现的时间已经不算短了,免费维护期马上也要到期了,官方已经开始推荐使用 Java11。

 

Java8 是革命性的一个版本,一直以来,Java 最受诟病的地方就是代码写起来很啰嗦,仅仅写一个 HelloWorld 都需要些很多的样板代码。

 

在 Java8 推出之后,啰嗦的代码有了很大的改观,Java 也可以写出简单优美的代码。最明显的改观就是 Java 开始支持函数式编程。

 

函数式编程的定义很晦涩,但是我们可以将函数式编程理解为函数本身可以作为参数进行传递,就是说,参数不仅仅可以是数据,也可以是行为(函数或者方法的实现其实就是逻辑行为)。

 

可能是 Java8 步子跨的太大,以至于现在还有很多人没有赶上来,依然用 Java8 在写 Java5 风格的代码。

 

这篇文章的目的就是彻底说清楚 Java8 的变化,以及快速全面的使用 Java8 的特性,让 Java 代码优雅起来。

 

函数式接口

在开始说 Java8 的函数式编程之前,我们需要说明一下,在 Java8 中新增加的一个概念,叫函数式接口。

 

这个函数式接口是 Java8 实现函数式编程的基础,正是这类接口的存在,才能把函数(方法)当做参数进行传递,至少表面上看起来是这样的,但是实际上传递的还是对象,这个问题我们下面再讨论,先回到函数式接口。

 

下面就是一个函数式接口:


public interface Action {
    public void action();
}

 

这个函数式看起来和普通的接口没有什么区别,唯一的区别是函数式接口只能有一个抽象方法。

 

如果你想让别人立马理解这个接口是函数式接口,可以加上 @FunctionalInterface 注解,这个注解不会提供任何额外的功能,仅仅用来表示这个接口是一个函数式接口。
 
@FunctionalInterface
public interface Action {
    public void action();
}

只能有一个抽象方法是为了更方便的把函数作为参数来传递,这个后面再细说。

 

我们可以根据自己的需要来定义函数式接口,JDK 为了使用方便,内置了很多函数式接口,日常使用完全够了。

 

常用的函数接口有:

Function
Predicate
Consumer

函数式接口其实就这么简单,看到这里你可能还是对函数式接口不是很理解,没关系,现在你仅仅只需要记住函数式接口就是模板。

 

Lambda 表达式

说起 Java8 的函数式编程,很多人都知道 lambda 表达式,这也是 Java8 中最容易被人记住的地方。

 

先来通过一个直观的例子来了解一下 lambda,在操作 ArrayList 等数据结构时,我们有可能要对其中的数据进行排序,比如:

Comparator<Integer> comparator = new Comparator<Integer>() {
    public int compare(Integer i1, Integer i2) {
        return i1.comparaTo(i2);
    }
};

在上面的代码中,真正有用的代码也有比较大小的那行,其他的都是样板代码。在这样的情况下,lambda 就很有用。

Comparator<Integer> comparator = (Integer i1, Integer i2) -> {return i1.compareTo(i2);}

 

这样看起来是不是很简单了,但是还是继续优化,可以把返回参数的部分也省略:

Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1.compareTo(i2)

既然两个参数都是 Integer 那是不是也可以省略,最后就得到了下面这样的形式:

Comparator<Integer> comparator = (i1, i2) -> i1.compareTo(i2)

这就是 lambda 的力量,可以把上面那么多的代码浓缩成一行。

 

lambda 其实就是一段代码,但也不仅仅是一段代码,再简单的 lambda 也会有三部分,参数列表,箭头和 lambda 主体,上面的 (i1, i2) 就是参数列表, i1.compareTo(i2) 就是 lambda 主体,箭头把这两部分隔开了。

 

lambda 是匿名的,这点和 Java 中的匿名实现类有点像(本质上一样),而且它是一种函数,不属于任何类(属于类的函数称之为方法),并且可以作为参数进行传递,而且还很简洁。

 

看到这里,可能就有人猜到函数式接口和 lambda 之间可能有某些关系了,没错,我们已经快说到最重要的部分。

 

在这之前再来理解一个概念,函数签名,函数签名为可以表示一类函数,如果两个函数的以下部分相同,就可以说这两个函数的签名一致:

函数参数及其类型
返回值及其类型
可能会抛出的异常
还有访问控制符(public等等)

最关键的地方来了,只要 lambda 和函数式接口方法的签名一致,lambda 表达式就可以作为参数传入到以该函数式接口为参数类型的方法中。

 

来看个详细的例子, Comparator 接口的定义如下:

  @FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

虽然 Comparator 中方法不止一个,但是抽象方法只有 compare 一个,上面的 lambda 完全可以作为 compare 方法的实现,实际上,lambda 表达式确实是作为函数式接口抽象方法的实现,而且,lambda 表达式为作为整个函数接口的实例。

 

到这里,真相大白,Java 8 的虽然支持了函数式编程,这不代表函数就是 Java中的一等公民了,每一个函数其实还是被包裹成一个对象,对象依然是 Java 中的一等公民。

 

所以简单来说,只要 lambda 的表达式的参数和返回类型可以与函数式接口中的抽象方法对的上,lambda 就可以作为该函数式接口的实现进行传递。

 

比如上面列举的几种函数式接口,其实就是对一些通用函数的抽象,比如 Function 函数式接口如下:

@FunctionalInterface
public interface Function<T,R> {
    R apply(T t);
}

这种就代表接受一个参数,返回另一个值的函数,只要满足这个要求的 lambda 表达式都可以作为 它的实现。

 

再比如 Predicate 接口,代表接受一个参数返回一个布尔值的函数:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Comsumer 接口表示接受一个参数,什么都不返回的函数:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

如果这样理解起来还是有点困难,那就把这些函数式接口理解为 lambda 表达式的类型。

 

类型检查和类型推断

在上面我们说到了只要函数式接口抽象方法的函数签名与 lambda 一致,那么就可以把 lambda 表达式作为该函数式接口的实现。

 

上面的例子中, lambda 的参数类型也是可以省略的,那么 Java 是如何判断 lambda 是否与函数式接口匹配呢?

 

如果 lambda 表达式中,参数和返回值的类型都省略之后,需要从使用 lambda 的上下文推断出来。如何理解 Java8 的函数式编程-鸿蒙开发者社区

方法引用

本来到这里应该就很完美了,lambda 够简洁,用它写代码,可以省略很多无用的样本代码,但是 lambda 也不完美,因为 lambda 表达式的代码很难复用,而且很多的 lambda 表达式仅仅就是调用了其他的方法。

 

这个时候,方法引用就可以派上用场了,比如上面的例子,其实仅仅就是调用了 Integer.compareTo() 方法:

Comparator<Integer> comparator = (i1, i2) -> i1.compareTo(i2) 

还可以简化成下面的样子:

Comparator<Integer> comparator = Integer::compareTo

使用方法引用的时候,要使用 :: ,而且任何方法都可以这样被引用,无论是静态方法还是实例方法。

 

方法引用可以被认为是 lambda 的语法糖,使用方法引用可以让代码更加简洁,更直观,看到方法引用的名称就能大概知道代码的逻辑,并且还可以对一些代码进行复用。

 

写出Java8 风格的代码

在 Java8 之后,很多代码的写法应该摒弃,下面列举一些常见的例子。

 

遍历 List

Java8 以前:

for(Integer i : list) {
    System.out.println(i);
}

Java8 及以后:

list.forEach(System.out::println); 

forEach 接收 Consumer 类型的函数,而 System.out.println() 刚好就符合要求。

 

遍历 Map
Java8 以前:

for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
 
    System.out.println("Key: " + entry.getKey() + ", Value:" + entry.getValue());
 
}


 

Java8 及以后:

 
map.forEach((k, v) -> {System.out.println("Key: "+ k +", Value: "+ v)});
 

这里的 forEach 不是接收 Consumer 类型的函数,而是接收 BiConsumer 类的函数,可以用来处理 map 这种包含 key-value 类型的数据,在大多数场景下,内置的函数式接口以及足够我们使用,只有在一些特殊的场景下才需要自己定制。

 

这里也没有现成的方法引用,所以就可以使用 lambda 表达式来实现。

 

执行异步任务

假如要使用线程池来执行任务。

Java8 以前: 

ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000));
 
executor.submit(new Runnable() {
    public void run() {
        System.out.println("Thread pool execute");
    }
}); 

 

Java8 以后:

ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000));
 
executor.submit(() ->{
    System.out.println("Thread pool execute");
});



也可以使用 lambda 来解决这个问题,Runnable 也是一个函数式接口。

 

文 / Rayjun

分类
已于2020-9-2 18:13:13修改
1
收藏
回复
举报
回复
    相关推荐