Ummm... Java8和lambda

逆流范式
• 阅读 2550

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

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

Ummm... Java8和lambda

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

函数式编程并不是一个新概念,诸如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

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

Ummm... Java8和lambda

虽然写起来简洁了,但是在本质上编译器会将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中,这也将是本系列文章的后续的关注。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
4年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
4年前
Java8 新增特性 Lambda表达式
               聊聊Lambda  背景:    早在2014年oracle发布了jdk8,在里面增加了lambda模块。于是java程序员们又多了一种新的编程方式:函数式编程,也就是lambda表达式。    以下整理关于Lambda表达式资料(转载地址:https:/
Wesley13 Wesley13
4年前
Java8—一万字的Lambda表达式的详细介绍与应用案例
  基于Java8详细介绍了lambda表达式的语法与使用,以及方法引用、函数式接口、lambda复合等Java8的新特性!文章目录1Lambda的概述2函数式接口2.1Consumer消费型接口2.2Supplier供给型接口2.3Function<T,R函数型接口
Wesley13 Wesley13
4年前
Java8新特性学习
1简述公司自年初终于开始使用java8作为项目的标准jdk,在开发过程中,逐渐认识到java8的很多新特性,确实很方便.其中内容的核心,在于函数式编程,即将函数本身作为对象参数去处理.其中涉及到三个关键新特性:1.lambda表达式(及函数式接口)2.stream3.方法引用这三个新特性的使用是相辅相
Wesley13 Wesley13
4年前
Java8的这些集合骚操作,你掌握了嘛?
Java8时Lambda表达式的出现,将行为作为参数传递进函数的函数式编程,大大简化了之前冗杂的写法。对于集合一类,我们来整理一下发生的变化吧。!Java8的这些集合骚操作,你掌握了嘛?(https://p6tt.byteimg.com/origin/dficimagehandler/e5ad919fe84f4ae7b7c8395f5
Wesley13 Wesley13
4年前
Java 8 stream 实战
概述平时工作用python的机会比较多,习惯了python函数式编程的简洁和优雅。切换到java后,对于数据处理的『冗长代码』还是有点不习惯的。有幸的是,Java8版本后,引入了Lambda表达式和流的新特性,当流和Lambda表达式结合起来一起使用时,因为流申明式处理数据集合的特点,可以让代码变得简洁易读。幸福感爆棚,有没有!本文主要列举一些
Wesley13 Wesley13
4年前
Java 8 中 Map 骚操作之 merge() 的用法分析
!(https://oscimg.oschina.net/oscnet/985add53402ea3e94310daaf1539cd50929.jpg)Java8最大的特性无异于更多地面向函数,比如引入了 lambda 等,可以更好地进行函数式编程。前段时间无意间发现了 map.merge() 方法,感觉还是很好用的,此文简单做一
逆流范式
逆流范式
Lv1
两岸猿声啼不住,轻舟已过万重山。
文章
4
粉丝
0
获赞
0