2

Java8引入了与此前完全不同的函数式编程方法,通过Lambda表达式和StreamAPI来为Java下的函数式编程提供动力。本文是Java8新特性的第一篇,旨在阐释函数式编程的本义,更在展示Java是如何通过新特性实现函数式编程的。

最近在读这本图灵的新书:Java 8 in Action ,本书作者的目的在于向Java程序员介绍Java8带来的新特性,鼓励使用新特性来完成更简洁有力的Java编程。本系列的文章的主要思路也来源于本书。

到底什么是函数式编程呢?

函数式编程并不是一个新概念,诸如Haskell这样的学院派编程语言就是以函数式编程为根基的,JVM平台上更彻底的采用函数式编程思维的更是以Scala为代表,因此函数式编程确实不是什么新概念。
下面来谈谈命令式编程和函数式编程。
什么是命令式编程呢?很容易理解,就是一条条的命令明明白白地告诉计算机,计算机依照这些这些明确的命令一步步地执行下去就好了,从汇编到C,这样的命令式编程语言无非都是在模仿计算机的机器指令的下达,明确地在每一句命令里面告诉计算机每一步需要怎么申请内存(对象变量)、怎么跳转到下一句命令(流转),即便后来的为面向对象编程思维而生的编程语言,比如Java,也仍然未走出这个范式,在每个类的对象执行具体的方法时也是按照这种“对象变量-流转”的模式在运行的。在这个模式下,我们会经常发现程序编写可能会经常限于冗长的“非关键”语句,大量的无用命令只是为了照顾语言本身的规则:比如所谓的面向接口编程最终变成了定义了一组一组的interface、interfaceImpl。
函数式编程则试图从编程范式的高度提高代码的抽象表达能力。命令式编程语言把“对象变量”和“流转”当作一等公民,而函数式编程在此基础上加入了“策略变量”这一新的一等公民。策略是什么呢?策略就是函数,函数本身是可以作为变量进行传递的。在以往的编程范式里,策略要被使用时通常是被调用,所以策略的使用必须通过承载策略的类或对象这样的对象变量,而函数式编程里面,我们可以直接使用策略对象来随意传递,省去了这些不必要的无用命令。
Java8作为一个新特性版本,在保留原有的Java纯面向对象特性之外,在容易理解的范围内引入了函数式编程方式。

引入策略:策略何以作为变量?

我们有这样的一个引入的例子:我们有一堆颜色和重量不定的苹果,这些苹果需要经过我们的一道程序,这道程序可以把这堆苹果中的红苹果取出来。怎样编写程序来选出红苹果呢?

首先我们定义苹果Apple类:

public class Apple{
    private String color;
    private Integer weight;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public Integer getWeight() {
        return weight;
    }

    public void setWeight(Integer weight) {
        this.weight = weight;
    }

    public Apple(String color, Integer weight) {
        this.color = color;
        this.weight = weight;
    }
}

添加我们的一堆颜色和重量随机的苹果:

public static void main(String[] args){
        ArrayList<Apple> apples = new ArrayList<>();
        Random weightRandom = new Random();
        Random colorRandom = new Random();
        String[] colors = {"red","green","yellow"};
        for (int i = 0; i < 100; i++) {
            apples.add(new Apple(colors[colorRandom.nextInt(3)],weightRandom.nextInt(200)));
        }
}

纯命令式的思路:

如果我们使用传统的命令式的编程方法,这个从苹果堆中筛选红苹果的方法会这样:

public static List<Apple> redAppleFilter(List<Apple> apples){
        List<Apple> redApples = new ArrayList<>();
        for (Apple apple:
             apples) {
            if("red".equals(apple.getColor())){
                redApples.add(apple);
            }
        }
        return redApples;
    }
List<Apple> redApples = redAppleFilter(apples);

如果这个时候我们变更需求了,比如我们不筛选红苹果了,要绿苹果了,怎么办呢?就得再定义一个从苹果堆中筛选绿苹果的方法:

public static List<Apple> greenAppleFilter(List<Apple> apples){
        List<Apple> greenApples = new ArrayList<>();
        for (Apple apple:
             apples) {
            if("green".equals(apple.getColor())){
                greenApples.add(apple);
            }
        }
        return greenApples;
    }
List<Apple> greenApples = greenAppleFilter(apples);

面向对象的编程方法:

使用为抽象操作而生的接口:接口只定义抽象的方法,具体的方法实现可以有不同的类来实现。如果把这些操作放到继承了一般筛选器的不同筛选方法的筛选器中去就会有一个典型的面向对象式的解决方案了:

interface AppleFilter {
    public List<Apple> filterByRules(List<Apple> apples);
}

class RedAppleFilter implements AppleFilter{

    @Override
    public List<Apple> filterByRules(List<Apple> apples) {
        List<Apple> redApples = new ArrayList<>();
        for (Apple apple:
                apples) {
            if("red".equals(apple.getColor())){
                redApples.add(apple);
            }
        }
        return redApples;
    }
}

class GreenAppleFilter implements AppleFilter{

    @Override
    public List<Apple> filterByRules(List<Apple> apples) {
        List<Apple> greenApples = new ArrayList<>();
        for (Apple apple:
                apples) {
            if("green".equals(apple.getColor())){
                greenApples.add(apple);
            }
        }
        return greenApples;
    }
}

面向对象的抽象层级问题

我们发现虽然使用了面向对象的编程方法虽然可以使得逻辑结构更为清晰:子类苹果筛选器实现了一般苹果筛选器的抽象方法,但仍然会有大量的代码是出现多次的。
这就是典型的坏代码的味道,重复编写了两个基本一样的代码,所以我们要怎么修改才能使得代码应对变化的需求呢,比如可以应对筛选其他颜色的苹果,不要某些颜色的苹果,可以筛选某些重量范围的苹果等等,而不是每个确定的筛选都需要编写独立且基本逻辑相同的代码呢?

我们来看一下重复的代码究竟是哪些:

List<Apple> greenApples = new ArrayList<>();
   for (Apple apple:
           apples) {
       ... ...
   }
   return greenApples;

不重复的代码有哪些:

if("green".equals(apple.getColor())){
    
}

其实对于循环列表这部分是对筛选这一逻辑的公用代码,而真正不同的是筛选的具体逻辑:根据红色筛选、绿色筛选等等。

而造成现在局面的原因就在于仅仅对大的筛选方法的实现的抽象层级太低了,所以就会编写太多的代码,如果筛选的抽象层级定位到筛选策略这一级就会大大提升代码的抽象能力。

传递策略对象

所谓策略的范围就是我们上面找到的这个“不重复的代码”:在这个问题里面就是什么样的苹果是可以经过筛选的。所以我们需要的这个策略就是用于确定什么样的苹果是可以被选出来的。我们定义一个这样的接口:给一个苹果用于判断,在test方法里对这个苹果进行检测,然后给出是否被选出的结果。

interface AppleTester{
    public Boolean test(Apple apple);
}

比如我们可以通过实现上述接口,重写这个test方法使之成为选择红苹果的方法,然后我们就可以得到一个红苹果选择器:

class RedAppleTester implements AppleTester{
    @Override
    public Boolean test(Apple apple) {
        return "red".equals(apple.getColor());
    }
}

再比如我们可以通过实现上述接口,重写这个test方法使之成为选择大苹果的方法,然后我们就可以得到一个大苹果选择器:

class BigAppleTester implements AppleTester{
    @Override
    public Boolean test(Apple apple) {
        return apple.getWeight()>150;
    }
}

有了这个选择器,我们就可以把这个选择器,亦即我们上面提到的筛选策略,传给我们的筛选器,以此进行相应需求的筛选,只要改变选择器,就可以更换筛选策略:

public static List<Apple> filterSomeApple(List<Apple> apples,AppleTester tester){
        ArrayList<Apple> resList = new ArrayList<>();
        for (Apple apple
                : apples) {
            if(tester.test(apple))
                resList.add(apple);
        }
        return resList;
    }
List<Apple> redApples = filterSomeApple(apples,new RedAppleTester());
List<Apple> bigApples = filterSomeApple(apples,new BigAppleTester());

通过使用Java的匿名类来实现选择器接口,我们可以不显式地定义RedAppleTester,BigAppleTester,而进一步简洁代码:

List<Apple> redApples = filterSomeApple(apples, new AppleTester() {
            @Override
            public Boolean test(Apple apple) {
                return "red".equals(apple.getColor());
            }
        });
List<Apple> bigApples = filterSomeApple(apples, new AppleTester() {
            @Override
            public Boolean test(Apple apple) {
                return apple.getWeight()>150;            
            }
        });

所以我们已经从上面的说明中看到,我们定义的策略是:一个实现了一般苹果选择器接口的抽象方法的特殊苹果选择器类的对象,因为是对象,所以当然是可以在代码里作为参数来传递的。这也就是我们反复提到的在函数式编程里的策略传递,在原书中叫做「行为参数化的目的是传递代码」

说到这里,其实这种函数式编程的解决思路并未出现什么Java8的新特性,在低版本的Java上即可实现这个过程,因为思路虽然很绕,但是说到底使用的就是简单的接口实现和方法重写。实际上呢,借助Java 8新的特性,我们可以更方便地使用语法糖来编写更简洁、更易懂的代码。

Java 8 Lambda简洁化函数式编程

我们上面定义的这种单方法接口叫做函数式接口

interface AppleTester{
    public Boolean test(Apple apple);
}

函数式接口的这个方法就是这个函数式接口的函数,这个函数的「参数-返回值」类型描述叫做函数描述符,test函数的描述符是 Apple->Boolean
而lambda表达式其实是一种语法糖现象,它是对函数实现的简单表述,比如我们上文的一个函数实现,即实现了AppleTester接口的RedAppleTester:

class RedAppleTester implements AppleTester{
    @Override
    public Boolean test(Apple apple) {
            return "red".equals(apple.getColor());
    }
}

这个实现类可以用lambda表达式
(Apple a) -> "red".equals(a.getColor())
或者
(Apple a) -> {return "red".equals(a.getColor());}
来代替。->前是参数列表,后面是表达式或命令。

在有上下文的情况下,甚至有更简洁的写法:
AppleTester tester = a -> "red".equals(a.getColor());
可以这样写的原因在于编译器可以根据上下文来推断参数类型:AppleTester作为函数式接口只定义了单一抽象方法:public Boolean test(Apple apple),所以可以很容易地推断出其抽象方法实现的参数类型。

如果AppleUtils工具类直接定义了判定红苹果的方法:

class AppleUtils {
    public static Boolean isRedApple(Apple apple) {
        return "red".equals(apple.getColor());
    }
}

我们会发现isRedApple方法的方法描述符和函数式接口AppleTester定义的单一抽象方法的函数描述符是一样的:Apple->Boolean,因此我们可以采用一种叫做方法引用的语法糖来进一步化简这个lambda表达式,不需要在lambda表达式中重复写已经定义过的方法:

AppleTester tester = AppleUtils::isRedApple

方法引用之所以可以起作用,就是因为这个被引用的方法具有和引用它的函数式接口的函数描述符相同的方法描述符。在实际创建那个实现了抽象方法的匿名类对象时会将被引用的方法体嵌入到这个实现方法中去:

虽然写起来简洁了,但是在本质上编译器会将lambda表达式编译成一个这样的实现了接口抽象方法的匿名类的对象。
基于lambda表达式简洁而强大的表达能力,可以很容易把上面的这段代码:

List<Apple> redApples = filterSomeApple(apples, new AppleTester() {
            @Override
            public Boolean test(Apple apple) {
                return "red".equals(apple.getColor());
            }
        });

改写为Java8版本的:

List<Apple> redApples = filterSomeApple(apples, AppleUtils::isRedApple);

如你所见,这样的写法瞬间将代码改到Java8前无法企及的简洁程度。

Java 8泛型函数式接口

我们在上文介绍的这个函数式接口:

interface AppleTester{
    public Boolean test(Apple apple);
}

它的作用仅仅是对苹果进行选择,通过实现test抽象方法来作出具体的选择器。
但是其实在我们的应用环境中,很多需求是泛化的,比如上文中的给一个对象(文中是苹果)以判断其是否能满足某些需求,这个场景一经泛化即可被许多场景所使用,可以使用泛型来对接口进行泛化:

interface ChooseStrategy<T>{
    public Boolean test(T t);
}

public Boolean test(T t)的函数描述符是T->Boolean,所以只要说满足这个描述符的方法都可以作为方法引用。

同时我们需要一个泛化的filter方法:

    public static <T> List<T> filter(List<T> ts, ChooseStrategy<T> strategy){
        ArrayList<T> resList = new ArrayList<>();
        for (T t
                : ts) {
            if(strategy.test(t))
                resList.add(t);
        }
        return resList;
    }
List<Apple> redApples = filter(apples,AppleUtils::isRedApple);

除了这种在类型上的泛型来泛化使用定义的函数式接口外,甚至有一些公用的场景Java8 为我们定义了一整套的函数式接口API来涵盖这些使用场景中需要的函数式接口。我们的编程中甚至不需要自己定义这些函数式接口:

  1. java.util.function.Predicate<T>
    函数描述符:T->boolean

  2. java.util.function.Consumer<T>
    函数描述符:T->void

  3. java.util.function.Function<T,R>
    函数描述符:T->R

  4. java.util.function.Supplier<T>
    函数描述符:()->T

  5. java.util.function.UnaryOperator<T>
    函数描述符:T->T

  6. java.util.function.BinaryOperator<T>
    函数描述符:(T,T)->T

  7. java.util.function.BiPredicate<L,R>
    函数描述符:(L,R)->boolean

  8. java.util.function.BiConsumer<T,U>
    函数描述符:(T,U)->void

  9. java.util.function.BiFunction<T,U,R>
    函数描述符:(T,U)->R

Java8通过接口抽象方法实现、lambda表达式来实现了策略对象的传递,使得函数成为了第一公民,并以此来将函数式编程带入了Java世界中。
有了策略传递后,使用具体的策略来完成任务,比如本文中筛选苹果的filter过程,Java8则依靠StreamAPI来实现,一系列泛化的任务过程定义在这些API中,这也将是本系列文章的后续的关注。


JinhaoPlus
1.5k 声望92 粉丝

扎瓦程序员