函数式编程 Functional Programming

函数式编程,早在 50 多年前就已经出现,近几年又逐渐引人关注,出现了很多新的函数式编程语言,比如 Clojure、Scala、Erlang 等。

一些非函数式编程语言也加入了很多特性、语法、类库来支持函数式编程,比如 Java、Python、Ruby、JavaScript 等。除此之外,Google Guava 也有对函数式编程的增强功能。

严格上来讲,函数式编程中的「函数」,并不是指我们编程语言中的「函数」概念,而是指数学「函数」或者「表达式」,比如,y=f(x)y=f(x)。不过,在编程实现的时候,对于数学「函数」或「表达式」,我们一般习惯性地将它们设计成函数。所以,如果不深究的话,函数式编程中的数学概念「函数」也可以理解为编程语言中的「函数」。

在科学计算、数据处理、统计分析这些领域,程序往往比较容易用数学表达式来表示,比起非函数式编程,实现同样的功能,函数式编程可以用很少的代码就能搞定。

函数式编程的核心特点是,函数作为一段功能代码,可以像变量一样进行引用和传递,以便在有需要的时候进行调用。

函数式编程和面向过程编程的区别

函数式编程的代码实现跟面向过程编程一样,也是以函数作为组织代码的单元。它跟面向过程编程的区别在于,函数式编程的函数是无状态的。函数内部涉及的变量都是局部变量,不会像 站内文章面向对象编程 那样,共享类成员变量,也不会像面向过程编程那样,共享全局变量。函数的执行结果只与入参有关,跟其他任何外部变量无关。同样的入参,不管怎么执行,得到的结果都是一样的。这实际上就是数学函数或数学表达式的基本要求。

1
2
3
4
5
6
7
8
9
10
// 有状态函数: 执行结果依赖b的值是多少,即便入参相同,多次执行函数,函数的返回值有可能不同,因为b值有可能不同。
int b;
int increase(int a) {
return a + b;
}

// 无状态函数:执行结果不依赖任何外部变量值,只要入参相同,不管执行多少次,函数的返回值就相同
int increase(int a, int b) {
return a + b;
}

Java 中的函数式编程

Google Guava 对函数式编程的使用是很谨慎的,认为过度地使用函数式编程,会导致代码可读性变差,强调不要滥用。

Functional Explained —— Google Guava

Excessive use of Guava’s functional programming idioms can lead to verbose, confusing, unreadable, and inefficient code. These are by far the most easily (and most commonly) abused parts of Guava, and when you go to preposterous lengths to make your code “a one-liner,” the Guava team weeps.

函数类型与 @FunctionalInterface

C 语言支持函数指针,它可以把函数直接当变量来使用。Java 没有函数指针这样的语法,所以它通过函数接口,将函数包裹在接口中当作变量来使用。

Java 对函数式编程的支持,本质是通过接口机制来实现的。首先定义一个仅声明一个方法的接口,然后对接口冠以 @FunctionalInterface 注解,那么这个接口就可以作为「函数类型」,可以接收一段以 Lambda 表达式,或者方法引用予以承载的逻辑代码。

@FunctionalInterface 标注的接口仅声明一个方法,一旦这个函数定义好,它能执行的功能是确定的,就是调用和不调用的区别。显然,这个方法可以用来代表「函数类型」所能执行的功能,接口中声明的方法就是和函数体定义一一对应的。

@FunctionalInterface 下只能声明一个未实现的方法,多一个、少一个都不能编译通过。但有以下例外:

  • 覆写 ObjecttoString/equals 的方法不受此个数限制。
  • default 方法和 static 方法因为带有实现体,所以也不受此限制。

@FunctionalInterface 注解不是必须的,不加这个注解的接口(前提是只包含一个方法)一样可以作为函数类型。不过,显而易见的是,加了这个注解表意更明确、更直观,是更被推荐的做法。

JDK 提供的函数类型

java.util.function 包下预定义了常用的函数类型,包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 无参无返回型函数可以使用 Runnable 接口

@FunctionalInterface
public interface Consumer<T> {
void accept(T t); //接收一个类型为T(泛型)的参数,无返回值;所以叫消费者
}

@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);//接收2个参数,无返回值
}

@FunctionalInterface
public interface Supplier<T> {
T get();//无参数,有返回值(所以叫提供者)
}

//注意没有BiSupplier,因为返回值只能有1个,不会有2个
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);//一个输入(参数),一个输出(返回值)
}

@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);//两个输入T和U,一个输出R
}

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {//一元操作,输入原样返回给输出
return t -> t;
}
}

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {//二元操作,输入输出类型相同
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;//传入比较器,返回较小者
}
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;//传入比较器,返回较大者
}
}

这些个定义,都是在参数个数(0、1、2 个)和有无返回值上做文章。另外还有一些将泛型类型具体化的衍生接口,比如 PredicateLongSupplier 等等。

1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);//输入1个参数,返回boolean,就好比是预言家,预言你这个输入是真还是假
}

@FunctionalInterface
public interface LongSupplier {
long getAsLong();//没有输入,输出long类型(long类型的提供者)
}

Lambda 表达式

Lambda 表达式写法

遍历集合除了 站内文章基本的方式 以外,还可以使用 Lambda 表达式。Lambda 表达式又称函数式表达,是 Java 中典型的语法糖。

Lambda 表达式语法:

1
(Type parameter, ... ) ->{ statements; }

parameters 是参数列表, { statements; } 是 Lambda 表达式的主体。

简化写法:

  • 参数简化:
    • parameters 如果只有一个参数,可以省略小括号 ();如果没有参数,也需要空括号 ()
    • parameters 可以省略参数类型。
  • 语句简化:
    • 无返回值:statements; 只包含一条语句,无返回值,可省略大括号 {}、分号 ;
    • 有返回值:
      • 一般写法:(int x)->{return x*x;}
      • 普通的表达式:(int i,int j)->(i*j+2)
      • 可省略括号:x->x*x
      • ❌错误写法:x->return x*x

Lambda 表达式代替匿名内部类

@FunctionalInterface 声明了函数类型,Lambda 表达式就用来定义函数类型的实现体。在介绍 Lambda 简化原理之前,首先回忆以下关于 Java 接口的知识。

Java 的 interface 接口有以下特点:

  • 接口不能被实例化,只能通过它的实现类来实现。
  • 不允许创建接口的实例,但允许定义接口类型的引用变量,该变量引用实现了这个接口类的实例。如:Photographable t=new Camera();,其中 Photographable 为接口类型,Camera 为实现了这个接口的类。
  • 可以使用匿名内部类创建接口类型的引用变量。匿名内部类中必须实现接口的所有方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Flyable 为接口类型
Flyable fly = new Flyable() {
@Override
public void shit() {
// TODO Auto-generated method stub
}

@Override
public void laugh() {
// TODO Auto-generated method stub
}

@Override
public void fly() {
// TODO Auto-generated method stub
}
};

在没有 Lambda 之前,有些方法要求传入接口类型的参数。我们需要先用匿名内部类创建接口类型的引用变量,再把它传进去。现在 Java 8 中可以通过 Lambda 语法糖快速实现一个单方法的匿名内部类。

更详细地说,Lambda 表达式只能赋值给声明为函数式接口的 Java 类型的变量(注解 @FunctionalInterface)。下文例子中的 RunnableConsumerComparator 都是被 @FunctionalInterface 标注的函数式接口,因此可以接受 Lambda 表达式。

例子 1:线程的创建

详看:站内文章Java 并发编程

例子 2:foreach

Java 集合都实现了 java.util.Iterable 接口。forEach() 方法有一个 Consumer 接口类型的 action 参数,它包含了对集合中每个元素的具体操作行为。action 参数所引用的 Consumer 实例必须实现 Consumer 接口的 accept(T t) 方法,在该方法中指定对参数 t 所执行的具体操作。

1
2
3
4
5
6
7
8
9
10
11
// Java Iterable 接口源代码
public interface Iterable<T> {
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}

// ...
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {

List<Person> persons = new ArrayList<Person>(){
{ //匿名类初始化代码
add(new Person("Tom",21));
add(new Person("Mike",32));
add(new Person("Linda",19));
}
};

persons.forEach( (Person p) ->{ //Lambda表达式,相当于是Consumer类型的匿名对象
//指定对每个元素的具体操作
p.setAge(p.getAge()+1);
System.out.println(p.getName()+":"+p.getAge());
}
);
}

以上 Lambda 表达式相当于创建了一个 Consumer 类型的匿名对象,并实现了 Consumer 接口的 accept(T t) 方法,此处传给 accept(T t) 方法的参数为 Person 对象。在 Lambda 表达式中符号 -> 后的可执行语句块相当于 accept(T t) 方法的方法体。

例子 3:排序

详看文章:站内文章Java 中的排序

例 4:与 Stream API 连用

Collection 接口的 stream() 方法返回一个 Stream 对象,程序可以通过这个 Stream 对象操纵集合中的元素。

Lambda 表达式可操纵的变量作用域

this 关键字实际上引用的是外部类的实例。

在 Lambda 表达式中访问的局部变量必须符合以下两个条件之一:

  • 条件一:最终局部变量,即用 final 修饰的局部变量。
  • 条件二:实际上的最终局部变量,即虽然没有被 final 修饰,但在程序中不会改变局部变量的值。

方法引用

在编译器能根据上下文来推断 Lambda 表达式的参数的场合,可以在 Lambda 表达式中省略参数,直接通过 :: 符号来引用方法。

方法引用就是对一个类中已经存在的方法加以引用,分以下类型:

  1. 引用类的静态方法
    • 对类构造方法的引用,如 ClassName::new
    • 对类静态方法的引用,如 ClassName::staticMethodName。编译器会根据上下文推断到底用哪个对象的实例方法。
  2. 引用实例方法
    • objectName::instanceMethod,或者 new Test()::instanceMethod
    • ClassName::instanceMethod。编译器会根据上下文推断到底用哪个对象的实例方法。

示例:

1
2
3
4
5
6
7
8
9
x -> new BigDecimal(x) // 等同于 BigDecimal::new
x->System.out.println(x) // 引用实例方法 相当于 System.out::println
(x,y)->Math.max(x,y) // 引用静态方法 相当于 Math::max
x->x.toLowerCase() // 引用实例方法 相当于 String::toLowerCase


// 以下两种 Lambda 表达式等价:
names.forEach((name)->System.out.println(name));
names.forEach(System.out::println);

本文参考