函数式编程

函数式编程概述

  1. Java 本身是面向对象编程的,而很多情况下只需要函数式编程即可,即尽量忽略面向对象的复杂语法,强调做什么而不是以什么形式去做,并不是所有的方法实现都有建立一套面向对象模型的必要,比如一些临时的只在一处用一次的实现方式,就不需要。
  2. Java 提供了一种函数式编程的固有模式,其三要素: 函数式接口 + 服务使用方 + Lambda表达式
    提供具体实现方式
    面向接口编程
    实际使用时传入
    Lambda表达式
    函数式接口
    服务使用方
  • 上面的这一套和用匿名内部类给普通接口提供具体实现方式,服务使用方面向普通接口编程,在 Java 的运行机制本质上是完全等价的。

Java 提供的新的这种函数式编程方式,运行时本质上仍然是基于它面向对象特性来实现的,比如 lambda 表达式,Java 自己会动态地给它加个 “类的壳子” ,再新建这个类的对象,然后把对象赋值给函数式接口的引用,函数的使用方再由此函数式接口引用多态地调用该实现对象的方法。只不过,Java 会把这些过程都自己做,程序员只需要写 lambda 表达式这个实现方式本身即可。

  • 但上面这一套使用 Lambda 表达式语法更加简洁,对程序员而言更加是函数式编程,并且 Java 提供了一些规范统一的函数式接口,也有利于代码的后期维护。
  • 这一套固有模式,我们可以自己去完整地设计和使用。并且,Java 提供的很多内部 API,就是基于这一套模式的,函数式接口和服务使用方都是 Java 定义好的接口和类,我们使用这些 API 时候,提供一个具体的实现方式(可用 Lambda 表达式)即可。

Lambda 表达式

概述

  1. Lambda 表达式介绍:
  • Lambda 表达式是函数式编程思想的体系,对于只定义了一个抽象方法的接口,当它的某种实现方式仅需要在某一处使用而并不需要复用时,可以使用 Lambda 表达式来简化书写这个实现方式的代码。这种用法与匿名内部类的使用类似。
  • 在底层 Java 会把它包装成一个接口实现类的对象然后使用多态特性,这个我们不需要关心。
    所以在编程角度,可以简单地直接把它看成一个实现类对象就行,可以作为方法的形参传入,也可以作为方法的返回值返回。
  1. Lambda 表达式和匿名内部类的对比:
    函数式接口
    它处已有实现方式情况下
    可选择
    任何均可
    任何均可
    任何均可
    使用: lambda表达式
    使用: 方法引用
    使用: 匿名内部类
    接口
    抽象类
    具体类
  • 所需类型不同:
  • 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类;
  • Lambda 表达式:只能是接口;
  • 使用限制不同:
  • 如果接口中有且仅有一个抽象方法,可以使用 Lambda 表达式,也可以使用匿名内部类;
  • 如果接口中多于一个抽象方法,只能使用匿名内部类,而不能使用 Lambda 表达式;
  • 实现原理不同:
  • 匿名内部类:编译之后,产生一个单独的 .class 字节码文件;
  • Lambda 表达式:编译之后,没有一个单独的 .class 字节码文件,对应的字节码会在运行的时候动态生成。

Lambda 语法

  1. 使用 Lambda 表达式必须满足以下要求:
  • 有一个接口且接口中有且仅有一个抽象方法(也就是函数式接口);
  • 必须有上下文环境,Java 才能推导出 Lambda 对应的接口。

Lambda 表达式所对应接口的推断:

  • 根据局部变量的赋值得知 Lambda 对应的接口,如:

Runnable r = () -> System.out.println("Lambda表达式");

  • 根据调用方法的形参类型得知 Lambda 对应的接口,如:

new Thread(() -> System.out.println("Lambda表达式")).start();

  1. Lambda 表达式的语法格式:
  1. 组成三要素: 形参、箭头、代码块
  2. 标准格式:
(形参列表) -> {代码块}
  • 形参:若有多个参数,参数之间用逗号隔开,若没有参数,留空即可;
  • 箭头:固定写法,代表指向动作;
  • 代码块:相当于方法体内容。
  1. 省略格式:( Java 可推导的就是可省略的)
  1. 形参类型可以省略不写,要注意整个形参列表要么都省略不写参数类型,要么都写;
  2. 若形参有且仅有一个,则形参外码的小括号可以省略不写;
  3. 如果代码块的语句只有一条,则代码块外面的大括号和这条语句的分号都可以省略不写;

若这条唯一的语句是 return 语句,那么 return 关键字也要省略不写。

方法引用

  1. 概述:
    在使用 Lambda 表达式时,要执行的操作在其它地方已有现成方案的情况下,使用 “方法引用” 语法可直接使用其它地方某个已有的具体方法来作为要执行的操作,而且相比 Lambda 表达式,它进一步省略了书写形参列表和箭头,所以可进一步简化代码。
  2. 使用语法和含义:
  • 引用类的静态方法:
类名::静态方法名

Lambda 表达式被这样替代时,它的形参全部传递给这个静态方法作为参数。

  • 引用类的成员方法:
类名::成员方法名

Lambda 表达式被这样替代时,它的形参列表中的第一个参数作为调用者,后面的全部传递给该方法作为参数。

  • 引用类的构造方法:
类名::new

Lambda表达式被这样替代时,它的形参全部传递给构造器作为参数。

  • 引用对象的成员方法:
对象引用::成员方法名

Lambda 表达式被这样替代时,它的形参全部传递给该方法作为参数。

常见函数式接口

  • 所谓函数式接口就是只含一个抽象方法的接口,可用 @FunctionalInterface 注解检查。比如,多线程的 Runnable 接口、集合排序的比较器 Comparator<T> 都是函数式接口。
  • java.util.function 包下专门定义了大量的函数式接口:
  • 在这个包下,Java 把每一类的操作定义都分别为一个函数式接口,并对特定一类的操作可能要用到的相关的辅助操作封装为一些 default 方法放在该函数式接口内,方便服务使用方在面向此接口编程时顺便选用。
  • 可以把它们作为一种大家共同遵守的函数式编程时的接口部分的规范。

定义的很多,下面选讲其中 4 个:

Supplier<T>

  • 概述:
    Supplier<T> 接口代表运算或其它操作结果的提供者,它也被称为生产型接口,它生产的数据的类型由泛型指定。
  • 方法:

方法

说明

T get()

按照某种实现逻辑(由 Lambda 表达式实现)得到一个 T 类型的数据

  • 举例:
import java.util.Scanner;
import java.util.function.Supplier;

public class JavaSE {
    public static void main(String[] args) {
        printUppercase(() -> "aBcDefg");
        printUppercase(() -> new Scanner(System.in).nextLine());
    }

    static void printUppercase(Supplier<String> sp) {
        String s = sp.get().toUpperCase();
        System.out.println(s);
    }
}

Consumer<T>

  • 概述:
    Consumer<T> 接口代表一种接收单个输入参数并且不返回结果的操作,它也被称为消费型接口,它消费的数据的类型由泛型指定。和多数函数式接口不同的是,Consumer<T> 接口一般要通过附属操作来操作。
  • 方法:

方法

说明

void accept(T t)

对给定的参数执行这个操作(由 Lambda 表达式实现)

default Consumer<T> andThen(Consumer<? super T> after)

返回一个包装了将自己的 accept 操作与 after 的 accept 操作顺序组合的 Consumer 对象,该对象调用 accept 的效果是依此执行两个操作

源码:

public interface Consumer<T> {
    void accept(T t);
    
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}
  • 举例:
import java.util.function.Consumer;

public class JavaSE {
    public static void main(String[] args) {
        operateString("你好哦", System.out::println, s -> {
            System.out.println(new StringBuilder(s).reverse());
        });
    }

    static void operateString(String s, Consumer<String> c1, Consumer<String> c2) {
        // 要依此进行c1、c2的操作的方式1:
        c1.accept(s);
        c2.accept(s);
        System.out.println("------");
        // 要依此进行c1、c2的操作的方式2:
        c1.andThen(c2).accept(s);
    }
}

/* 控制台输出:

你好哦
哦好你
------
你好哦
哦好你

*/

Predicate<T>

  • 概述:
    Predicate<T> 接口代表一种对传入参数进行判定真假并返回结果的操作,常用于判断给定的参数是否满足某种要求。
  • 方法:

常用方法

说明

boolean test(T t)

根据给定的参数(由 Lambda 表达式操作)计算出判断结果

default Predicate<T> negate()

返回会得到原判断结果的逻辑非的结果的 Predicate 对象

default Predicate<T> and(Predicate<? super T> other)

返回本判断结果和另一个判断结果的与

default Predicate<T> or(Predicate<? super T> other)

返回本判断结果和另一个判断结果的或

源码:

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

    @SuppressWarnings("unchecked")
    static <T> Predicate<T> not(Predicate<? super T> target) {
        Objects.requireNonNull(target);
        return (Predicate<T>)target.negate();
    }
}
  • 举例:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class JavaSE {

    public static void main(String[] args) {
        ArrayList<Student> source = new ArrayList<>();
        source.add(new Student("Jack", 22));
        source.add(new Student("Rose", 20));
        source.add(new Student("XiaoMing", 19));
        source.add(new Student("ZhangSan", 17));
        List<Student> resultList = filterStudent(source, student -> student.getAge() >= 18, student -> student.getName().length() <= 4);
        for (Student s : resultList) {
            System.out.println(s.getName());
        }
    }

    static List<Student> filterStudent(List<Student> list, Predicate<Student> pre1, Predicate<Student> pre2) {
        ArrayList<Student> result = new ArrayList<>();
        for (Student s : list) {
            if (pre1.and(pre2).test(s)) {
                result.add(s);
            }
        }
        return result;
    }

}


/* 控制台输出:

Jack
Rose

*/

Function<T, R>

  • 概述:
    Function<T, R> 接口代表某个接收一个参数然后产生一个结果的函数,常用于对参数进行处理及转换(由 Lambda 表达式实现)然后返回一个结果。
  • 方法:

常用方法

说明

R apply(T t)

将给定的参数传入函数(由 Lambda 表达式操作计算)然后返回结果

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)

返回一个将前一个函数的结果作为后一个函数的输入即组合它们操作的 Function 对象

Stream 流

Stream 流是 Java 提供的函数式编程的固有模式的应用之一,它用函数式编程来进行集合/数组这样的数据容器内的数据操作。

  1. 生成流:
    通过数据源(集合、数组等)生成流:
  • 数组:通过 Stream 接口的静态方法 of(T... values) 生成流。
  • Collection 体系集合:使用 Collection 接口的默认方法 stream() 生成流。
  • Map 体系集合:使用 entrySet() 方法得到键的 Set 集合,再用它间接地生成流。
  1. 中间操作:
    一个流后面可以跟随零或多个中间操作,做出某种程度的数据过滤/映射,然后返回一个新的流,交给下一个操作使用。

常用方法

说明

Stream<T> filter(Predicate<? super T> pre)

根据条件进行过滤

Stream<T> limit(long maxSize)

只取前几个元素

Stream<T> skip(long n)

跳过(丢弃)前几个元素

Stream<T> distinct()

将流中元素去重(由 Object.equals()

Stream<T> sorted()

将流中元素自然排序

Stream<T> sorted(Comparator<? super T> comparator)

将流中元素根据给定的比较器进行排序

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

将流中元素分别传入给的函数得到结果的流

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

合并两个流的元素

这里只列举了部分返回 Stream 流的方法,还有些方法返回其它的专用用途的操作流,可查文档使用。

  1. 终结操作:
    一个流只能有一个终结操作,当这个操作执行之后,流就被使用光了(不再返回新的流了),无法再被操作了。

常用方法

说明

void forEach(Consumer<? super T> action)

对流中的每个元素都进行传入的操作