# 函数式编程

TIP

没有共享的可变数据,以及将方法和函数(即代码)传递给其他方法的能力,这两个要点是函数式编程范式的基石

# 一、名词解释

# 1.外部迭代

Collection API:用for-each循环一个个地迭代元素,然后再处理元素。我们把这种数据迭代方法称为外部迭代

# 2.内部迭代

有了Stream API,你根本用不着操心循环的事情。数据处理完全是在库内部进行的。我们把这种思想叫作内部迭代。

# 3.方法引用(行为参数化)

比方说,你想要筛选一个目录中的所有隐藏文件。你需要编写一个方法,然后给它一个File,它就会告诉你文件是不是隐藏的。幸好,File类里面有一个叫作isHidden的方法。可以把它看作一个函数,接受一个File,返回一个布尔值。但要用它做筛选,需要把它包在一个FileFilter对象里,然后传递给File.listFiles方法,如下所示:

File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
    public boolean accept(File file) {
        return file.isHidden();---- 筛选隐藏文件
    }
});
1
2
3
4
5

虽然只有三行,但这三行可真够绕的。我们第一次碰到的时候肯定都说过:“非得这样不可吗?”已经有一个方法isHidden可用,为什么非得把它包在一个啰唆的FileFilter类里面再实例化呢?因为在Java 8之前你必须这么做!如今在Java 8里,你可以把代码重写成这样:

File[] hiddenFiles = new File(".").listFiles(File::isHidden);
1

你已经有了函数isHidden,因此只需用Java 8的方法引用::语法(即“把这个方法作为值”)将其传给listFiles方法。

只要方法中有代码(方法中的可执行部分),那么用方法引用就可以传递代码。

# 一个例子

假设你有一个Apple类,它有一个getColor方法,还有一个变量inventory保存着一个Apples列表。你可能想要选出所有的绿苹果(此处使用包含值GREENREDColor枚举类型 ),并返回一个列表。通常用筛选(filter)一词来表达这个概念。在Java 8之前,你可能会写这样一个方法filterGreenApples




 






public static List<Apple> filterGreenApples(List<Apple> inventory){
    List<Apple> result = new ArrayList<>();---- result是用来累积结果的List,开始为空,然后一个个加入绿苹果
    for (Apple apple: inventory){
        if (GREEN.equals(apple.getColor())) {---- 加粗显示的代码会仅仅选出绿苹果
            result.add(apple);
        }
    }
    return result;
}
1
2
3
4
5
6
7
8
9

但是接下来,有人可能想要选出重的苹果,比如超过150克的苹果,于是你心情沉重地写了下面这个方法,甚至用了复制粘贴:




 






public static List<Apple> filterHeavyApples(List<Apple> inventory){
    List<Apple> result = new ArrayList<>();
    for (Apple apple: inventory){
        if (apple.getWeight() > 150) {---- 这里加粗显示的代码会仅仅选出重的苹果
            result.add(apple);
        }
    }
    return result;
}
1
2
3
4
5
6
7
8
9

我们都知道软件工程中复制粘贴的危险——给一个做了更新和修正,却忘了另一个。嘿,这两个方法只有一行不同:if里面加粗的那行条件。如果这两个加粗的方法之间的差异仅仅是接受的重量范围不同,那么你只要把接受的重量上下限作为参数传递给filter就行了,比如指定(150, 1000)来选出重的苹果(超过150克),或者指定(0, 80)来选出轻的苹果(低于80克)。

但是,前面提过了,Java 8会把条件代码作为参数传递进去,这样可以避免filter方法中出现重复的代码。现在你可以写:

public static boolean isGreenApple(Apple apple) {
    return GREEN.equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
    return apple.getWeight() > 150;
}
public interface Predicate<T>{---- 写出来是为了清晰(平常只要从java.util.function导入就可以了)
    boolean test(T t);
}
static List<Apple> filterApples(List<Apple> inventory,
                                Predicate<Apple> p) {---- 方法作为Predicate参数p传递进去(见附注栏“什么是谓词?”)
    List<Apple> result = new ArrayList<>();
    for (Apple apple: inventory){
        if (p.test(apple)) {---- 苹果符合p所代表的条件吗
            result.add(apple);
        }
    }
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

要用它的话,你可以写:

filterApples(inventory, Apple::isGreenApple);
1

或者

filterApples(inventory, Apple::isHeavyApple);
1

# 4.谓词

前面的代码传递了方法Apple::isGreenApple(它接受参数Apple并返回一个boolean)给filterApples,后者则希望接受一个Predicate<Apple>参数。谓词(predicate)在数学上常常用来代表类似于函数的东西,它接受一个参数值,并返回true或false。后面你会看到,Java 8也允许你写Function<Apple,Boolean>——在学校学过函数却没学过谓词的读者对此可能更熟悉,但用Predicate<Apple>是更标准的方式,效率也会更高一点儿,这避免了把boolean封装在Boolean里面。

# 5.默认方法

Java 8之前,List<T>并没有stream或parallelStream方法,它实现的Collection<T>接口也没有,因为当初还没有想到这些方法嘛!可没有这些方法,这些代码就不能编译。换作你自己的接口的话,最简单的解决方案就是让Java 8的设计者把stream方法加入Collection接口,并加入ArrayList类的实现。

可要是这样做,对用户来说就是噩梦了。有很多集合框架都用Collection API实现了接口。但给接口加入一个新方法,意味着所有的实体类都必须为其提供一个实现。语言设计者没法控制Collection所有现有的实现,这下你就进退两难了:你如何改变已发布的接口而不破坏已有的实现呢?

Java 8的解决方法就是打破最后一环——接口如今可以包含实现类没有提供实现的方法签名了!那谁来实现它呢?缺失的方法主体随接口提供了(因此就有了默认实现),而不是由实现类提供。

这就给接口设计者提供了一种扩充接口的方式,而不会破坏现有的代码。Java 8在接口声明中使用新的default关键字来表示这一点。

例如,在Java 8里,你可以直接对List调用sort方法。它是用Java 8 List接口中如下所示的默认方法实现的,它会调用Collections.sort静态方法:

default void sort(Comparator<? super E> c) {
    Collections.sort(this, c);
}
1
2
3

这意味着List的任何实体类都不需要显式实现sort,而在以前的Java版本中,除非提供了sort的实现,否则这些实体类在重新编译时都会失败。

# 6.Java中的函数

Java 8中新增了函数,作为值的一种新形式。

想想Java程序可能操作的值吧。首先有原始值,比如42(int类型)和3.14(double类型)。其次,值可以是对象(更严格地说是对象的引用)。获得对象的唯一途径是利用new,这也许是通过工厂方法或库函数实现的;对象引用指向一个类的实例。例子包括"abc"(String类型)、new Integer(1111)(Integer类型),以及new HashMap<Integer,String>(100)的结果——它显式调用了HashMap的构造函数。甚至数组也是对象。那么有什么问题呢?

为了帮助回答这个问题,我们要注意到,编程语言的整个目的就在于操作值,按照历史上编程语言的传统,这些值应被称为一等值(或一等公民)。编程语言中的其他结构也许有助于表示值的结构,但在程序执行期间不能传递,因而是二等值。前面所说的值是Java中的一等值,但其他很多Java概念(比如方法和类等)则是二等值。人们发现,在运行时传递方法能将方法变成一等值。这在编程中非常有用,因此Java 8的设计者把这个功能加入到了Java中。

# 7.流处理

是一系列数据项,一次只生成一项。程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能是另一个程序的输入流。

# 8.Lambda——匿名函数

除了允许(命名)函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想,包括Lambda5 (或匿名函数)。比如,你现在可以写(int x) -> x + 1,表示“调用时给定参数x,就返回x+1值的函数”。你可能会想这有什么必要呢?因为你可以在MyMathsUtils类里面定义一个add1方法,然后写MyMathsUtils::add1嘛!确实是可以,但要是你没有方便的方法和类可用,新的Lambda语法更简洁。

3.方法引用(行为参数化)把方法作为值来传递显然很有用,但要是为类似于isHeavyAppleisGreenApple这种可能只用一两次的短方法写一堆定义就有点儿烦人了。不过Java 8也解决了这个问题,它引入了一套新记法(匿名函数或Lambda),让你可以写

filterApples(inventory, (Apple a) -> GREEN.equals(a.getColor()) );
1

或者

filterApples(inventory, (Apple a) -> a.getWeight() > 150 );
1

甚至

filterApples(inventory, (Apple a) -> a.getWeight() < 80 || RED.equals(a.getColor()) );
1

# 9.菱形继承

待完成

# 10.匿名类

匿名类和你熟悉的Java局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时声明并实例化一个类。换句话说,它允许你随用随建。

# 二、新类

# 2.1 Optional<T>

# 三、示例

# 1.按照重量给inventory中的苹果排序

// Java7
Collections.sort(inventory, new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2){
        return a1.getWeight().compareTo(a2.getWeight());
    }
});

// Java8
inventory.sort(comparing(Apple::getWeight));
1
2
3
4
5
6
7
8
9

# 2.你需要从一个列表中筛选金额较高的交易,然后按货币分组。

-- Java7
Map<Currency, List<Transaction>> transactionsByCurrencies =
    new HashMap<>();---- 建立累积交易分组的Map
for (Transaction transaction : transactions) {---- 遍历交易的List
    if(transaction.getPrice() > 1000){---- 筛选金额较高的交易
        Currency currency = transaction.getCurrency();---- 提取交易货币
        List<Transaction> transactionsForCurrency =
            transactionsByCurrencies.get(currency);
        if (transactionsForCurrency == null) {---- 如果这个货币的分组Map是空的,那就建立一个
            transactionsForCurrency = new ArrayList<>();
            transactionsByCurrencies.put(currency,
                                         transactionsForCurrency);
        }
        transactionsForCurrency.add(transaction);---- 将当前遍历的交易添加到具有同一货币的交易List}
}

-- Java8
import static java.util.stream.Collectors.groupingBy;
Map<Currency, List<Transaction>> transactionsByCurrencies =
    transactions.stream()
                .filter((Transaction t) -> t.getPrice() > 1000)---- 筛选金额较高的交易
                .collect(groupingBy(Transaction::getCurrency));---- 按货币分组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
更新时间: 3/27/2023, 2:43:42 PM