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

函数类型与 @FunctionalInterface

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
@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);

本文参考