Java 函数式编程学习总结

函数式编程学习总结

为什么需要函数式编程

java的抽象级别不够,在处理大型数据集合的时候,java欠缺高效的并行操作。

OOP(Object Oriented Programming,面向对象编程)是对数据进行抽象,而FP(Functional Programming,函数式编程)是对行为进行抽象。

在现实世界中,数据和行为并存,程序也是如此,因此两种编程方式都要学习。

函数式编程这种新的抽象方式还有其他好处:程序员能够编写出更容易阅读的代码——这种代码更多的表达了业务逻辑的意图,而不是它的实现机制。

Lambda表达式

形如:

  • () -> {实现逻辑};

lambda表达式实例

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test_lambda1() {
Runnable noArguments = () -> System.out.println("Hello world");

Runnable noArgumentMulti = () -> {
System.out.println("Hello");
System.out.println("World");
};

Thread thread = new Thread(noArgumentMulti);
thread.start();
}

方法引用(Function Reference)

语法为:Classname::methodName ,其中 :: 操作符被称为方法引用操作符。

凡是能够使用lambda标准表达式x -> x.method()的情况下,都能够使用方法引用。

1
2
3
4
5
6
7
8
public static void main(String[] args) {

var result = Stream.of(1,2,3,4,5,6)
.map(x -> x + 2)
.reduce(0,Math::max);

System.out.println(result);
}

重要的函数接口

函数式编程最重要的是 方法签名 ,它声明了函数的入参和出参,是函数式编程的基石。

  • Predicate
  • Consumer
  • Function<T,R>
  • Supplier
  • UnaryOperator
  • BinaryOperator
  • @FunctionalInterface

#TODO#

什么是流

流指随着时间产生的数据序列

java8 Stream:指为一组序列提供顺序、并行的计算

  • 支持函数式编程
  • 提供管道运算能力
  • 提供并发并行(parallel)计算能力
  • 提供大量操作

Stream是用函数式编程方式在集合上进行复杂操作的工具

惰性求值

惰性求值(lazy evaluation):最终不产生新集合(或新返回值),指描述Stream的方法,叫惰性求值方法。

实例:

1
2
3
4
5
6
7
public static void main(String[] args) {

var list = Arrays.asList(1, 2, 3, 4, 5);

// 仅描述Stream,返回的仍是Stream
var result = list.stream().filter(x -> x >= 3);
}

及早求值:最终会从Stream产生值的方法,叫做及早求值。

这种最终产生值的方法,称为终结操作/方法

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {

var list = Arrays.asList(1, 2, 3, 4, 5);

// 仅描述Stream,返回的仍是Stream
var result = list.stream().filter(x -> x >= 3)
.count();

}

判断方法:

如果返回值是Stream(Pipe Line),则是惰性求值;如果返回值是另一个值或者空,则为及早求值。

整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,最后用一个 build 方法,这时对象才被真正创建。

of 与 Optional

在面向对象的编程中,对象是一等公民,一般通过 new 关键字来创建对象,基本类型也都有对应的包装类。

在函数式编程中,函数是一等公民,需要一个专属的返回值和创建流的方法。

  • of 用于构造自己的流,是流的工厂方法

  • Optional 是流中专属的返回值,用来替换 null

    • 把Stream中的返回值进行装箱,防止各种异常的产生,使流计算更加安全
1
2
3
4
5
6
7
8
9
@Test
public void test_reduce() {
var result = Stream.of(1, 2, 3, 4, 5)
.reduce((x, y) -> {
return x + y;
});

System.out.println(result.get());
}

【注意】可以通过 orElse 方法,避免返回 Optional

常用的流操作

【注意】以下例子均为最简单实例,均可以在方法中调用自定义的函数。

collect

collect方法有stream里的值生成一个列表,是一个及早求值操作

1
2
3
4
5
6
7
public static void main(String[] args) {

var list = Arrays.asList(1, 2, 3, 4, 5);

var result = list.stream().filter(x -> x > 3)
.collect(Collectors.toList());
}

map

如果有一个函数可以将一种类型的值转换为另一种类型,map操作就可以使用该函数,将一个流中的值转换成一个新的流。

1
2
3
4
5
6
7
8
public static void main(String[] args) {

var result = Stream.of("ashe", "zed", "yasuo")
.map(x -> x.toUpperCase())
.collect(Collectors.toList());

System.out.println(result);
}

filter

遍历数据并检查/过滤其中元素时,可以尝试使用该方法。见 collect 中的例子组合使用

flatmap

flatmap方法可以用stream替换值,然后将多个stream连接成一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test_flapmap() {

var list1 = Arrays.asList(1, 2);
var list2 = Arrays.asList(3, 4);

var togetherList = Stream.of(list1, list2)
.flatMap(x -> x.stream())
.collect(Collectors.toList());

System.out.println(togetherList);

}

max & min

求最值

1
2
3
4
5
6
7
8
9
10
@Test
public void test_max() {
var list = Arrays.asList(1, 2, 4, 5, 6);

var result = list.stream()
.max(Comparator.comparing(x -> x))
.orElse(0);

System.out.println(result);
}

reduce

该方法可以从一组值中生成一个值,如累加等

1
2
3
4
5
6
7
8
9
10
@Test
public void test_reduce() {
var list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

var result = list.stream()
.reduce(0, (x, y) -> {
return x + y;
});
System.out.println(result);
}

sorted

使用sorted方法能够使流中的数据按从小到大的顺序排列;

1
2
3
4
5
6
7
8
public static void main(String[] args) {

var result = Stream.of(1,2,3,4,5,6,3,6,11)
.sorted()
.collect(Collectors.toList());

System.out.println(result);
}

unordered

有一些流的操作会使数据自动排序。一些操作在有序的流上开销会更大,而使用该方法能消除这种自动排序的操作。

【注意】以下实例中的map并不会引起数据的自动排序,实例并不是十分准确。

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {

var result = Stream.of(1,2,3,4,5,6,3,6,11)
.map(x -> x -1)
.unordered()
.collect(Collectors.toList());

System.out.println(result);
}

partitioningBy

该方法用于将流分解为两个集合,一部分为符合条件的集合,另一部分为不符合条件的集合。

要注意的是partitioningBy里面的返回值必须为boolean类型

1
2
3
4
5
6
7
public static void main(String[] args) {

var result = Stream.of("Ashe", "Ashen one", "Zed", "Yasuo")
.collect(Collectors.partitioningBy(x -> x.substring(1, 2).equals("A")));

System.out.println(result);
}

groupingBy

该方法用于将数据分割为多个list

该方法与SQL中的group by操作蕾丝,只是在Stream类库中将其实现了

1
2
3
4
5
6
7
@Test
public void test_groupingBy() {
var map = Stream.of("Ashe", "Zed", "Yasuo")
.collect(groupingBy(x -> x));

System.out.println(map.toString());
}

并行处理

以并行计算为例

image.png

coding实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 流并行计算实例
@Test
public void test_parallel() {

// 获取随机数生成器
var r = new Random();

// 从数组中获取最大值
var list = IntStream.range(0, 80_000_000)
.map(x -> r.nextInt(80_000_000))
.boxed()
.collect(Collectors.toList());

// 串行计算
var seqStartTime = System.currentTimeMillis();
System.out.println("Seq Max value:" + list.stream().reduce(0, Math::max));
System.out.println("Seq Time:" + (System.currentTimeMillis() - seqStartTime));

// 并行计算
var parStartTime = System.currentTimeMillis();
System.out.println("Parallel Max value:" + list.parallelStream().reduce(0, Math::max));
System.out.println("Parallel Time:" + (System.currentTimeMillis() - parStartTime));

}

结果:

image.png

并行流性能的因素

1.性能指标

  • 数据分块的大小:当数据足够大,每个数据处理管道花费的时间足够多时,并行化处理才有意义
  • 装箱:处理基本类型比处理装箱类型要快,如果调用了box方法,则会触发自动装箱,进而导致性能会下降
  • cpu核数:拥有的可用核数越多,性能提升的幅度越大。可以通过方法Runtime.getRuntime().availableProcessors() 来获取可用核数
  • 单元处理开销:花费在每个元素上的时间越长,并行操作带来的性能提升越明显

2.可以根据性能的好坏,将容器中的通用数据结构分成三组

  • 性能好:ArrayList、数组

    • 支持随机读取,可以很好地被分割
  • 性能一般:HashSet、TreeSet

    • 不易公平的被分解,但是大多数时候是可能的
  • 性能差:LinkedList、Stream.lierate、BufferedReader.lines

    • 长度未知、不支持随机读取、难以预测分割的位置

3.如果能避开有状态的操作,选用无状态的操作,能够获得更好的性能

  • 无状态操作(stateless)

    • filter
    • flatmap
    • map
  • 有状态操作(stateful)

    • sorted
    • distinct
    • limit
    • skip

4.在函数式编程的时候,多使用纯函数(pure function)

  • 纯函数指无副作用的函数,即纯计算的但一操作,不涉及其他操作的函数

  • 非纯函数实例

5.函数式编程性质总结

  • 一般使用纯函数(pure function),要求输入的数据不可变(immutable),仅仅对输入的数据做拷贝
  • 惰性求值(lazy)
  • 安全(Monad - safety)

这里的c属于外部变量,但是却在函数内部发生了计算,同时调用了I/O的操作,一旦发生异常,需要同时排查变量ab、变量c、IO设备。

image.png

串行处理

强制流串行处理

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {

var result = Stream.of(1, 2, 3, 4, 5).sequential()
.map(x -> x / 2)
.reduce(Math::max)
.orElse(0);

System.out.println(result);
}

当 sequential 方法和 parallel 方法同时使用时,只有一个会生效。以最后一个调用的方法为准。

整合操作

map -> filter -> reduce

image.png

业务场景实例

实例一:

新旧写法的比较

image.png

实例二:

image.png

实例三:

并行计算最大值的区别

1
2
3
var result = list.parallelStream().max((a, b) -> a - b).orElse(0);

var result = list.parallelStream().reduce(0,Math::max);

区别:

parallel()是把一个Stream变成parallelStream

parallelStream是把一个非stream(如 List)变成 parallelStream

Stream还是ParallelStream的max方法都是用reduce实现。

parallelStream的reduce方法有3个参数:(identity, accumulator, combiner)

  • identity 初始值
  • accumulator 单个线程如何累计,max就是一直算最大值
  • combiner就是如何合并多个线程计算的结果s

参考书籍

《Java8 函数式编程》

《函数式编程思想》

0%