Java函数式编程:Lambda表达式&函数式接口&Stream API

这周在看项目代码的时候,发现对于集合的处理,基本上都采用流处理的方式,于是决定周末对Java 8中此部分内容进行总结学习。

一般通过编程实现一个系统有两种思考方式。一种专注于如何实现,比如“首先做这个,紧接着更新那个,然后…….”。这种方式非常适合经典的面向对象编程,也称之为命令式编程。另一种更加关注于要做什么,具体实现留给函数库,这种方式称之为声明式编程。

函数式编程具体实践了声明式编程和无副作用计算,是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,只要输入确定,输出就是确定的。简单来说,函数内部涉及的变量都是局部变量,不会像面向对象编程那样,共享类成员变量。

函数式编程

作为面向对象的编程语言,Java 8开始支持函数式编程,并引入了Lambda表达式、函数接口和Stream API。

Lambda表达式

Lambda表达式可以看做一种简洁的可传递匿名函数,简化了代码的编写。它没有名称,但是有参数列表、函数主体和返回类型。

基本语法

Lambda表达式由参数列表,箭头和Lambda主体组成,主体中隐含return,也就是Lambda表达式的返回值。可以分为两种风格:

  • 表达式-风格的Lambda:(parameters) -> expression
  • 块-风格的Lambda:(parameters) -> { statements; }

一些Lambda的使用案例

  • 布尔表达式:(List<String> list) -> list.isEmpty()
  • 创建对象:() -> new Apple(10)
  • 消费对象:(Apple a) -> { System.out.println(a.getWeight()); }
  • 从对象中选择/抽取:(String s) -> s.length()
  • 组合两个值:(int a, int b) -> a * b
  • 比较两个对象:(Apple a1, Apple a2) -> a1.getWright().compareTo(a2.getWeight())

方法引用

方法引用可以被看做是调用特定方法的Lambda表达式的一种快捷写法,其基本思想是,如果一个Lambda表达式代表的只是“直接调用这个方法”,那么最好还是用名称来调用它,而不是如何调用它。因此,方法引用可以看作是针对仅仅单一方法的Lambda表达式的语法糖。一些例子如下:

Lambda 方法引用
(Apple apple) -> apple.getWeight() Apple::getWeight
() -> Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack
(str, i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) System.out::println
(String s) -> this.isValidName(s) this::isValidName

方法引用主要三类:

  1. 指向静态方法的方法引用:
    • Lambda: (args) ->ClassName.staticMethod(args)
    • 方法引用: ClassName::staticMethod
  2. 指向任意类型实例方法的方法引用:
    • Lambda: (arg0, rest) -> arg0.instanceMethod(rest)
    • 方法引用: ClassName::instanceMethod
  3. 指向现存对象或表达式实例方法的方法引用
    • Lambda: (args) -> expo.instanceMethod(args)
    • 方法引用: expr::instanceMethod

函数式接口

定义

函数式接口就是只定义一个抽象方法的接口,但是可以有其他多个默认方法,例如:

1
2
3
4
5
6
// 注解表明此接口为函数式接口
@FunctionalInterface
public interface Runnable {
// 唯一抽象方法
public abstract void run();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface Comparator<T> {
// 唯一抽象方法
int compare(T o1, T o2);

// Object类中的抽象方法不被视为接口的单一抽象方法
boolean equals(Object obj);
// 默认方法
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}

// 其他默认方法...
}

Lambda表达式允许以内联的方式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。相对于采用匿名内部类来实现抽象方法,Lambda表达式更加简洁。

  • 使用匿名内部类

    1
    2
    3
    4
    5
    Runnable r1 = new Runnable(){
    public void run() {
    System.out.println("Hello World");
    }
    };
  • 使用Lambda表达式

    1
    Runnable r2 = () -> System.out.println("Hello world");

常见函数式接口

Java 8中的java.util.function包中引入了Predicate、Consumer和Function等接口。

Predicate接口中的抽象方法是boolean test(T t),入参是一个泛型T对象,返回结果为boolean值。通常用于需要表示一个涉及类型T的布尔表达式,例如创建filter方法(Stream API中的方法)。

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
@FunctionalInterface
public interface Predicate<T> {

/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
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();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for (T t : list) {
if (p.test(t)) {
results.add(t);
}
}
return results;
}

public static void main(String[] args) {
List<String> nonEmpty = filter(
Arrays.asList("apple", "", "xiaomi", "", "huawei"),
(String s) -> !s.isEmpty()
);
// 输出3
System.out.println(nonEmpty.size());
}

Consumer接口中抽象方法是void accept(T t),入参是泛型T的对象,没有返回结果。通常用于需要访问类型T的对象,并对其执行某些操作的情况,例如可以用来创建forEach方法(Stream API中的方法)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@FunctionalInterface
public interface Consumer<T> {

/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);

default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public static <T> void forEach(List<T> list, Consumer<T> c) {
for (T t : list) {
c.accept(t);
}
}

public static void main(String[] args) {
forEach(
Arrays.asList(1,2,3,4,5),
// (Integer num) -> System.out.println(num)
System.out::println
);
}

Function接口中抽象方法是R apply(T t),入参是泛型T的对象,返回结果是泛型R的对象。通常用于将输入对象的信息映射到输出中,例如可以用来创建map方法(Stream API中的方法)。

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
@FunctionalInterface
public interface Function<T, R> {

/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}

static <T> Function<T, T> identity() {
return t -> t;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for (T t : list) {
result.add(f.apply(t));
}
return result;
}

public static void main(String[] args) {
List<Integer> list = map(
Arrays.asList("apple", "xiaomi", "huawei"),
// (String s) -> s.length()
String::length
);
}

Stream API

Java 8中的java.util.stream.Stream是对集合(Collection)对象功能的增强,专注于对集合对象进行各种便利、高效的操作。

流简介

流可以看做是遍历数据集的高级迭代器,与集合使用的显示迭代不同,流的迭代在后台进行,且可以透明地并行处理,无需写多线程代码。流会使用一个提供数据的源,如数组、集合等,流的数据处理功能类似于数据库的操作以及函数式编程中的常用操作。通常流操作本身会返回一个流,作为下一个流操作的输入,从而形成流水线。例如:

1
2
3
4
5
6
7
8
9
10
11
List<String> threeHighCaloricDishNames =
// 从menu中获得流
menu.stream()
// 建立操作流水线,选出热量高的菜肴
.filter(dish -> dish.getCalories() > 300)
// 获取菜名
.map(Dish::getName)
// 只选择前三个
.limit(3)
// 将结果保存到另一个List中
.collect(Collectors.toList());

在本例中,数据源是菜肴列表menu,通过调用stream方法,得到menu的一个流,之后的filter(筛选)、map(提取)、limit(截断)操作,都会返回另一个流,最终,由collect操作处理返回一个List。

流操作

java.util.stream.Stream接口中定义了许多流操作,总的来说,可以分为中间操作和终端操作。

流操作

除非流水线上触发一个终端操作,否则中间操作不会执行任何处理(懒处理)。

1
2
3
4
5
6
7
8
9
10
11
12
List<String> threeHighCaloricDishNames =
menu.stream()
.filter(dish -> {
System.out.println("filtering:" + dish.getName());
return dish.getCalories() > 300;
})
.map(dish -> {
System.out.println("mapping:" + dish.getName());
return dish.getName();
})
.limit(3)
.collect(Collectors.toList());

执行此代码将打印:

1
2
3
4
5
6
7
filtering:pork
mapping:pork
filtering:beef
mapping:beef
filtering:chicken
mapping:chicken
[pork, beef, chicken]

终端操作会从流的流水线生成结果,其结果可以是不是流的任何值。

常用操作

java.util.stream.Stream#map是stream最常用的一个转换方法,用于将流中的每一个元素映射转换到另一个流中,例如将流中的每个元素乘以2后保存到另一个流中。

1
2
3
4
5
6
7
8
public class MapTest {
public static void main(String[] args) {
Arrays.asList(1, 2, 3, 4)
.stream()
.map((Integer num) -> num * 2)
.forEach(System.out::println);
}
}

java.util.stream.Stream#filter也是stream中常用的一个转换方法,就是对流中的每一个元素按照条件进行过滤,将满足条件的元素保存的另一个流中,例如过滤流中大于0的元素。

1
2
3
4
5
6
7
8
9
public class FilterTest {
public static void main(String[] args) {
Arrays.asList(-3, -2, -1, 0, 1, 2, 3, 4)
.stream()
.filter((Integer num) -> num > 0)
// 打印1,2,3,4
.forEach(System.out::println);
}
}

在前文中介绍到map方法是由Function接口实现,filter方法是由Predicate接口实现,除此之外,java.util.function中的一些其他函数式接口也是Stream API中方法的实现基础。java.util.stream.Stream#reduce是stream中常用的聚合方法,也就是把流中的元素按照规则聚合成一个结果,而不是转换成另一个流。它基于java.util.function.BinaryOperator接口实现,与前面的接口类似,可以直接阅读源码即可。

1
2
3
4
5
6
7
public static void main(String[] args) {
int sum = Arrays.asList(1, 2, 3, 4, 5)
.stream()
.reduce(2, (acc, i) -> acc + i);
// 打印17,也就是2+1+2+3+4+5的结果
System.out.println(sum);
}

java.util.stream.Stream#collect同样是stream中常用的聚合方法,用于将流中元素输出为List、Map等,collect方法入参是一个Collector实例,例如Collectors.toList()或者Collectors.toMap()等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CollectTest {
public static void main(String[] args) {
List<String> list = Arrays.asList("apple", "xiaomi", " ", "", "huawei")
.stream()
.filter(s -> !s.isEmpty() && !s.isBlank())
.collect(Collectors.toList());
// 打印"apple" "xiaomi" "huawei"
list.stream().forEach(System.out::println);
Map<String, String> map = Arrays.asList("first:apple", "second:xiaomi", "third:huawei")
.stream()
.collect(Collectors.toMap(
// key
s -> s.substring(0, s.indexOf(':')),
// value
s -> s.substring(s.indexOf(':') + 1)
));
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
}

小结

函数式编程一方面能够简化代码的编写,另一方面可以实现代码透明地并行处理,提升了运行效率。在实际开发中,在条件允许的情况下,可以尽量采用Lambda表达式、流处理等。

参考