函数式编程之 Lambda 表达式的引出_Java8 实践

来源:互联网 发布:邵阳学院教务网络系统 编辑:程序博客网 时间:2024/05/29 14:41

前排提示, 文章很长, 但是看完了相信会对 java8 Lambda 表达式引出的过程有一个深刻的理解

一. 背景

计算机科学的发展通常是间歇性的, 好的思路有时要被搁置数十年后才突然间变成主流, 第一种面向对象的语言 Simula 诞生于 1967 年, 但是直到 1983 年的 C++, 面向对象的语言才终于开始流行, 同面向对象的编程一样, 函数式编程也诞生于学院, 然后在数十年后开始发力, 慢慢的浸染所有主流的编程语言. java8 中所做出的一切改变也都是为了适应这种潮流.

二. 演化_转变思维

java8 出现之前, java 程序员为了处理数据集合不得不编写大量的迭代, 为了传递函数不得不编写大量的匿名类, 为了写出应对高并发的代码不得不绞尽脑汁. 而 java8 的出现让这一切变得灵活而简洁.
下面我们结合实例一步一步的讲解从面向对象编程到函数式编程的演化, 首先我们需要讲解一个名词: 行为参数化, 什么叫做行为参数化, 简而言之就是将函数或者代码块作为参数传递, 大部分情况下我们编写方法传递的参数都是值, 而这里我们想让代码块也成为参数, 这样编写的代码灵活性更高.

数据集: 首先给出我们的业务场景: 这是我们之后要操作的数据, 是一个社交网络的花名册, 里面有多个用户, 每个用户都拥有姓名, 性别, 邮箱, 生日等属性.

问题: 按照给出的条件过滤指定的数据, 然后对过滤出来的数据进行处理.
例子: 过滤出年龄在 18 ~ 30 周岁之间的男性客户, 并且将其名字打印出来.

package newProperty.functional.lambda;import lombok.Getter;import java.time.LocalDate;import java.time.Period;import java.time.chrono.Chronology;import java.time.chrono.IsoChronology;import java.util.ArrayList;import java.util.List;/* * 1. @description: Lambda 表达式测试场景类: Person * 2. social networking application: 客户类*/public class Person {    /*    * @description: 性别枚举    */    public enum Sex {        MALE, FEMALE    }    @Getter String name;    @Getter Sex gender;    @Getter String emailAddress;    LocalDate birthday;    Person(String nameArg, LocalDate birthdayArg,           Sex genderArg, String emailArg) {        name = nameArg;        birthday = birthdayArg;        gender = genderArg;        emailAddress = emailArg;    }    /*    * @description: 获取年龄    */    public int getAge() {        return birthday                .until(IsoChronology.INSTANCE.dateNow())//birthday 与当前日期的差值                .getYears();    }    /*    * @description: 打印姓名: 年龄    */    public void printPerson() {        System.out.println(name + ": " + this.getAge());    }    /*    * @description: 获取生日    */    public LocalDate getBirthday() {        return birthday;    }    /*    * @description: 比较年龄大小 -1, 1, 0    */    public static int compareByAge(Person a, Person b) {        return a.birthday.compareTo(b.birthday);    }    /*    * @description: 社交网络花名册    */    public static List<Person> createRoster() {        List<Person> roster = new ArrayList<>();        roster.add(                new Person(                        "Fred",                        IsoChronology.INSTANCE.date(1980, 6, 20),                        Person.Sex.MALE,                        "fred@example.com"));        roster.add(                new Person(                        "Jane",                        IsoChronology.INSTANCE.date(1990, 7, 15),                        Person.Sex.FEMALE, "jane@example.com"));        roster.add(                new Person(                        "George",                        IsoChronology.INSTANCE.date(1991, 8, 13),                        Person.Sex.MALE, "george@example.com"));        roster.add(                new Person(                        "Bob",                        IsoChronology.INSTANCE.date(2000, 9, 12),                        Person.Sex.MALE, "bob@example.com"));        return roster;    }}
  • 我们按照常用的写法, 写一个迭代, 遍历给出的列表, 过滤之后对剩余数据做出处理, 这里面有关键的两个步骤: 过滤 + 处理
    /*    * @description: 1. 按一个条件查询: 找年龄大于 18 岁的打印    */    public static void printPersonsOlderThan(List<Person> roster, int age) {        for (Person person : roster) {            if (person.getAge() > age) {                person.printPerson();            }        }    }
  • 很遗憾, 需求不可能总是一尘不变的, 这时候我们的需求发生了改变, 我们需要找出一个年龄区间的所有客户进行处理.
    /*    * @description: 2. 增加查询条件: 需要修改代码    */    public static void printPersonsWithinAgeRange(List<Person> roster, int low, int high) {        for (Person person : roster) {            if (low < person.getAge() && person.getAge() < high) {                person.printPerson();            }        }    }
  • 于是为了应对这种变化, 我们将过滤部分抽象出来编写一个接口 CheckPerson, 这个接口只有一个叫做 test() 的方法, 我们可以编写它的实现类用于完成所有的业务逻辑.
    /*    * @description: 3. 增加条件需要不断修改原始代码 --> 增加本地类, 专门负责编写查询条件    */    public static void printPersons(List<Person> roster, CheckPerson tester) {        for (Person person : roster) {            if (tester.test(person))//打印满足条件的 person            {                person.printPerson();            }        }    }
  • 当然更简便的方式是我们直接用匿名类实现接口, 然后作为参数进行传递, 这里我们给方法传入了一个过滤行为. 但是很显然, 这种代码可读性不高, 当我第一次面对匿名函数的时候确实有些不知所措.
    /*    * @description: 4. 使用匿名类实现 CheckPerson 接口, 编写查询条件    */    public static void printPersonsAnonymous(List<Person> roster) {        printPersons(roster,  new CheckPerson() {            @Override            public boolean test(Person person) {                return person.getGender() == Person.Sex.MALE                        && person.getAge() >= 30                        && person.getAge() <= 50;            }        });    }
  • 于是为了让代码看起来简洁直观, 我们引入了终极大Boss Lambda 表达式, 这种代码首次看到感觉很抽象, 不知道什么意思, 但是相信我, 当你真正了解了它的语法后, 你会爱上它.

Lambda 表达式的语法很简单:

这里写图片描述

函数的入参的类型编译器会根据传递接口的抽象方法的函数描述符(简单理解为方法的入参类型出参类型),自动进行推断检查, 返回值类型也会自动的推断, 都可以不显示给出, 代码只关注业务逻辑的实现, 除此之外所有的无关模板代码都使用 java8 给的语法糖, 这样的编程体验真的让人感觉很爽.

    /*    * @description: 5. 功能性接口可以使用 Lambda 表达式代替匿名类实现    */    public static void printPersonsLambda(List<Person> roster) {        printPersons(roster,                person -> person.getGender() == Person.Sex.MALE                        && person.getAge() >= 30                        && person.getAge() <= 40        );    }
  • 之前我们的接口 CheckPerson是自己实现的, 它只有唯一的一个抽象方法 test(), 我们称这样的接口为函数化(功能性)接口, Lambda 表达式的任务就是为了给函数化接口的抽象方法编写具体实现, 有了 java8 默认的一些函数化接口, 可以匹配丰富的函数描述符, 我们大多数的时候都可以直接使用这些接口, 然后根据它们的函数描述符写出自己的 Lambda 表达式作为实现, 这些函数化接口大量使用 java5 引入的泛型, 通用性更好, 而且针对原始类型(基本数据类型)也给出了大量函数化接口, 比如 IntPredicate, IntToLongFunction, 它们避免了自动装箱拆箱带来的性能损失, 提高了性能.
    /*    * @description: 使用 JDK 默认功能性接口 Predicate, 返回一个 boolean 值作为条件判断结果, 实现类实现 Predicate 接口完成条件录入    */    public static void printPersonsPredicate(List<Person> persons, Predicate<Person> tester) {        for (Person person : persons) {            if (tester.test(person)) {                person.printPerson();            }        }    }

至此, 我们通过一个三步走战略引入了 Lambda 表达式, 上述例子我们已经看到了它的威力, 结合 java8 给出的函数化接口, Lambda 表达式让我们编写的代码清晰直观, 灵活简洁, 我们不再需要去编写与逻辑无关的模板代码. 下面通过一幅图给出这种演化的过程.

这里写图片描述

最初我们的目的是让行为参数化(传递组合代码块, 入参, 出参都可能是代码块), 没有 Lambda 表达式之前, 我们为了灵活性不得不牺牲可读性, 选择编写接口的实现类, 用对象来传递行为, 但是 java8 的到来让我们可以传递的参数粒度更细, 我们可以直接传递组合函数, 这样不仅让代码更加灵活, 而且更加简洁直观, 可读性更高.

三. 总结:

当然, 这样简短的篇幅远远不能将整个 Lambda 表达式的好处一一讲述清楚, 这里只是一个简单的入门, 想要更加深入的理解 Lambda 表达式, java8 的特性以及函数式编程的思想需要读者阅读大量的书籍, 做大量的练习, 这里我们大概的通过代码来推导出 Lambda 表达式出现的目的以及带来的好处. 关于 Lambda 表达式其实我们还有很多地方可以学习深究, 下面我给出几个简单的学习方向.
(1) 深入理解 java8 给出的大量函数化接口(Predicate, Function, Consumer), 理解函数描述符.
(2) 理解 Lambda 表达式的类型检查机制, 和类型推断机制(为什么即使不给出数据类型也能正常通过编译)
(3) 学习 Lambda 表达式的方法引用: 方法引用可以轻松的复用我们编写的 Lambda 表达式, 让代码看起来更加简洁, 毕竟在我认为 Lambda 表达式的出现就是为了让我们简化编程, 从命令式编程思维向函数式编程思维迈进.
(4) 学习 Lambda 表达式的复合, 我们都知道 Predicate 谓词的存在是为了过滤数据, 但是我们有时候过滤条件可能不止有一个, 当多个过滤条件出现的时候, 我们会编写多个 Lambda 表达式, 我们希望这些条件可以进行一些逻辑运算: 与或非, 但是我们知道函数化接口里面有且仅能存在一个抽象方法, 这时候 java8 的设计者们引入了另外一个非常重要的语法糖: 接口的默认实现, 在接口中编写实现方法, 完成这些附加功能, 接口的实现者不需要去理会这些默认的实现.

0 0