函数式编程

Java8 较之前版本 Java 最主要的改变是「函数式编程」的引入与实践。

1 函数式编程

函数式编程的定义(wikipedia):

函数式编程(英语:functional programming)或称函数程序设计,又称泛函编程,是一种编程范式,它将计算机运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。
比起指令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

函数式编程可能产生的副作用(wikipedia):

函数式编程常被认为严重耗费CPU和存储器资源。主因有二

  • 在实现早期的函数式编程语言时并没有考虑过效率问题。
  • 面向函数式编程特性(如保证函数参数不变性等)的独特数据结构和算法。

从函数式编程的定义看,函数式编程思想的核心在于:

  1. 函数。
  2. 声明式编程。

1.1 函数

需要注意的是,「函数式编程」对「函数」的定义不是通常所讲的「类中的方法」,这里的函数是指数学中的函数定义:对于同样参数的输入,总会返回同样的结果,结果不会随调用次数的改变而改变。从这个角度,可以说「函数」是无状态的。

从另一个角度讲,函数是无副作用的,函数应避免修改共享状态或执行其它有副作用的操作(如改变输入)。

举个例子来讲:

static List<List<Integer>> concat(List<List<Integer>> a,
                                      List<List<Integer>> b) {
        a.addAll(b);
        return a; 
}

    static List<List<Integer>> concat(List<List<Integer>> a,
                                      List<List<Integer>> b) {
        List<List<Integer>> r = new ArrayList<>(a);
        r.addAll(b);
        return r;
}

在上面两段代码中,下面的是一种纯粹的函数式编程方式。(这里只说风格与编程范式,忽略方式二带来的内存和时间消耗弊端)

1.2 声明式编程

「声明式编程」是针对「命令式编程」而言,如函数式编程的定义所述,函数式编程关注的是执行的结果而非过程,而声明式编程就是拿到结果的阐述过程。如下,针对于第一种命令式编程方式,第二段声明式编程代码以一种「做什么的陈述过程」简洁方式得到结果,其他的交给函数库和内部迭代。

代码一

Transaction mostExpensive = transactions.get(0);
    if(mostExpensive == null)
throw new IllegalArgumentException("Empty list of transactions")
for(Transaction t: transactions.subList(1, transactions.size())){ if(t.getValue() > mostExpensive.getValue()){
            mostExpensive = t;
        }
}

代码二

Optional<Transaction> mostExpensive =
    transactions.stream()
                .max(comparing(Transaction::getValue));

2 函数式编程实践

2.1 行为参数化

行为参数化是指将行为(代码)通过参数传递给类或方法。

下面是一个 filter 的例子:

public static List<Apple> filterApplesByWeight(List<Apple> inventory, int weight) {
        List<Apple> result = new ArrayList<Apple>();
        For (Apple apple: inventory){
if ( apple.getWeight() > weight ){ result.add(apple);
} }
        return result;
    }

下面将过滤行为(代码)封装起来,并以参数形式传递给 filter 方法,实现更好的「扩展」。

 public interface ApplePredicate{
        boolean test (Apple apple);
} 

public class AppleHeavyWeightPredicate implements ApplePredicate { 
    public boolean test(Apple apple){
        return apple.getWeight() > 150;
    }
}

public static List<Apple> filterApples(List<Apple> inventory,
                                           ApplePredicate p){
        List<Apple> result = new ArrayList<>();
        for(Apple apple: inventory){
            if(p.test(apple)){
                result.add(apple);
            }
        }
    return result;
}

更进一步,如果传递的行为代码只会使用一次,可以使用 匿名类 完成,如

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
     public boolean test(Apple a){
        return "red".equals(a.getColor()); 
    }
});

在 Java8 中,可以借助 lambda 进一步简化上述代码:

List<Apple> result =
    filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

2.2 lambda

什么是 lambda?

lambda 通常是指 lambda表达式,其概念来源于lambda演算,lambda 表达式可以支持以简洁的形式 表示一个行为或者传递代码

在 Java 中,所有的 函数式接口 都可以用 lambda 表达式表示,那么什么是函数式接口呢?简单来说,就是「只定义一个抽象方法的接口」。可以在 java.util.function 找到所有 JDK 自带的函数式接口。如:

2.3 Stream - 函数式数据处理

Java8 中提供了流式数据处理 API - Stream。Stream API 支持以声明式进行数据处理。如:

List<String> lowCaloricDishesName =
            menu.stream()
            .filter(d -> d.getCalories() < 400) 
            .sorted(comparing(Dish::getCalories))
            .map(Dish::getName)
            .collect(toList());

的概念包含以下几部分:

  • 源。流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集 合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作。流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中 的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
  • 流水线。很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。这让我们下一章中的一些优化成为可能,如延迟和短路。流水线的操作可以看作对数据源进行数据库式查询。
  • 内部迭代。与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。我们在第1章中简要地提到了这个思想,下一节会再谈到它。

流的使用一般包括三件事:

3 总结

函数式编程是一种使用数学意义上函数进行编程的思想。其关注无状态、声明式。Java8 引入了 lambda 作为对函数式编程的支持,并内置了流式处理库 Stream 支持数据的函数式处理。

4 其它

Java 8 还提供了新的日期和时间 API。众所周知,Java8 之前的关于时间的API除了不具有线程安全性,在易用性方面也大打折扣,比如你希望在一个日期的基础上增加一天,在之前的API中,这个简单的要求却引入一个复杂的实现。所以,在涉及到日期或时间处理时,大部分都会使用三方包如 joda-time。

新的 java.time 包中提供了 LocalDate、LocalTime、Instant、Duration和Period。

LocalDate 与 LocalTime 分别为日期与时间 API。

Period: A date-based amount of time in the ISO-8601 calendar system, such as ‘2 years, 3 months and 4 days’.

Duration: A time-based amount of time, such as ‘34.5 seconds’.

Instant: An instantaneous point on the time-line. This class models a single instantaneous point on the time-line. This might be used to record event time-stamps in the application.