b1cat`s

函数式编程

Word count: 2.6kReading time: 11 min
2020/02/24

函数式编程

函数是面向过程的程序设计的基本单元,而函数式编程(function Programming)其思想更接近数学计算。

而数学上的计算越抽象离计算机硬件越远,执行效率就低。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量。由于函数内部变量状态不确定,输入输出不确定,这种称为有副作用的。

函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称为Lambda Calculus。

Lambda表达式

函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。

Comparator为例,想要调用Arrays.sort()时,传入一个Comparator实例,以匿名类方式编写如下:

1
2
3
4
5
6
String[] array = ...
Arrays.sort(array, new Comparator<String>(){
public int compare(String s1, String s2){
return s1.compareTo(s2);
}
});

改用Lamabda表达式

1
2
3
4
String[] array = new String[] {"apple", "ipad", "mbp"};
Arrays.sort(array, (s1, s2)->{
return s1.compareTo(s2);
});
  • Lamabda 表达式,只需要写出方法定义(s1, s2)->{return s1.compare(s2);}
  • 参数类型可以省略,编译器会自动推断出String
  • ->{...}表示方法体,如果只有一行return xxx,可进一步简化为Arrays.sort(array, (s1, s2)-> s1.compareTo(s2));
  • 返回值类型也由编译器自动推断

FunctionalInterface

把只定义单方法的接口称为FuncationalInterface,用注解@FunctionalInterface标记。例如Callable接口:

1
2
3
4
@FunctionalInterface
public interface Callable<v> {
v call() throws Exception;
}

方法引用

除了Lambda表达式,还可以传入方法引用。如

1
2
3
4
5
6
7
8
9
10
public class Main{
public static void main(String[] args){
String[] array = new String[]{"apple", "ipad", "mbp"};
Arrays.sort(array, Main::cmp); //用Main::cmp直接传入静态方法引用
}

static int cmp(String s1, String s2){
return s1.compareTo(s2);
}
}

所谓方法引用就是,如果某个方法签名和接口恰好一直,就可以传入方法引用。

因为Comparator<String>接口定义方法是int compare(string, string)除了和静态方法cmp方法名不同其他都一直,即签名一致。

构造方法引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
System.out.println(persons);
}
}

class Person {
String name;
public Person(String name) {
this.name = name;
}
public String toString() {
return "Person:" + this.name;
}
}

后面我们会讲到Streammap()方法。现在我们看到,这里的map()需要传入的FunctionalInterface的定义是:

1
2
3
4
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}

把泛型对应上就是方法签名Person apply(String),即传入参数String,返回类型Person。而Person类的构造方法恰好满足这个条件,因为构造方法的参数是String,而构造方法虽然没有return语句,但它会隐式地返回this实例,类型就是Person,因此,此处可以引用构造方法。构造方法的引用写法是类名::new,因此,此处传入Person::new

使用Stream

Java 8开始不但引入Lambda表达式,还引入了全新的流式API,Stream API ,位于java.util.stream包中。不同于InputStream,它代表的是任意Java对象序列。

java.util.stream

  • 存储,顺序输出的任意Java对象实例
  • 用途,内存计算/业务逻辑
  • 元素,可能未分配、实时计算
  • 用途:惰性计算

举个例子:

比如要表示一个全体自然数的集合,因为自然数无限无法用List表示出来,但是Stream可以做到

1
Stream<BigInteger> naturals = createNaturalStream(); 

暂时不考虑createNaturalStream()是如何实现的,看看如何使用。

先将这个Steam中每个自然数做个平方,得到另一个Steam。

1
Stream<BigInteger> streaNxN = naturals.map(n->n.multiply(n)); //平方

因为这个有无穷的元素,要打印它需要变成有限个元素,可以用limit()方法截取,然后循环打印。

1
2
3
naturals.map(n -> n.multiply(n)) // 不计算
.limit(100) // 不计算
.forEach(System.out::println) // 计算

惰性计算的特点是:一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生。

因此Stream API的基本用法是:创建一个Stream,然后做若干次转换,最后调用一个求值方法获取真正计算的结果。

创建Stream

  • 使用Stream.of()静态方法,传入可变参数创建一个能输出确定元素的Stream。
1
2
Stream<String> stream = Stream.of("A", "B", "C");
stream.forEach(System.out::println); //forEach相当于内部循环调用

虽然这种方式基本上没啥实质性用途,但测试的时候很方便。

  • 基于数组或Collection
1
2
3
4
Stream<String> stream1 = Arrays.stream(new Stringp[]{"A", "B", "C"});
Stream<String> stream1 = List.of("A", "B", "C").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
  • 基于Supplier
1
Stream<Integer> natual = Stream.generate(new NatualSupplier());

基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。

  • 其他方法

因为Java的规范不支持基本类型,所以无法用Stream<int>, 为了保存int, 只能用String<Integer>但会产生频繁的装箱和拆箱。为了提高效率,Java标准库提供了IntStreamLongStreamDoubleStream这三种使用基本类型的Stream,使用方法和泛型Stream没有打的区别。

1
IntStream is = Arrays.Stream(new int[] {1,2,3});

使用map

Strean.map()Stream常见的一个转换方法,它把一个Stream转化为另一个Stream。

所谓map就是把一种操作运算,映射到一个序列的每一个元素上。

1
2
Stream<Integer> s = Stream.of(1,2,3,4,5);
Stream<Integer> s2 = s.map(n -> n*n);

查看Stream的源码,会发现map()方法接受的对象是Function接口对象,它定义了一个apply()方法, 负责把一个T类型转换为R类型:

1
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
1
2
3
4
@FuncationalInterface
public interface Function<T, R>{
R apply(T t);
}

通过若干步map转换,可以写出逻辑简单、清晰的代码。

使用filter

Stream.filter()Stream的另一个常用转换方法。

所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被”过滤”掉,身下的满足条件的元素就构成了一个新的Stream

比如:

1
2
3
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(n -> n % 2 != 0)
.forEach(System.out::println);

filter()方法接收的对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件:

1
2
3
4
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}

使用reduce

map()和filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。

1
2
int sum = Stream.of(1,2,3,4,5,6,7,8,9).reduce(0, (acc, n)-> acc + n);
System.out.println(sum); // 45

reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法。上面例子中负责把上次累加的结果和本次的元素相加,返回累加的结果。

1
2
3
4
@FunctionalInterface
public interface BinaryOperator<T>{
T apply(T t, T u);
}

reduce()是聚合方法,聚合方法会立刻对Stream进行计算。

输出

Stream可以输出为集合。

输出为List

因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转化操作,而是一个聚合操作,它会强制Stream输出每个元素。

调用collect(Collectors.toList())Stream的每个元素收集到List

1
2
3
Stream<String> stream = Stream.of("apple", "", null, "pear");
List<String> list = stream.filter(s -> != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);

输出为数组

把Stream的元素输出为数组和输出为List类似,只需要调用toArray()方法,并传入数组的构造方法

1
2
List<String> list = List.of("apple", "ipad", "iphone");
String[] array = list.stream().toArray(String[]::new);

传入的构造方法String[]::new,签名实际上是IntFunction<String[]>定义的String[] apply(int), 即传入int参数,获得String[]数组的返回值。

输出为Map

添加到Map时需要key和value,因此需要指定两个映射函数,分别把元素映射为key和value:

1
2
3
4
5
6
7
Stream<String> stream = Stream.of("A:Apple", "MS:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
s->s.substring(0, s.indexOf(":")),
s0>s.substring(s.indexOf(":")+1)
));
System.out.print(map);

分组输出

分组输出使用Collectors.groupingBy()

1
2
3
4
5
6
7
8
9
List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");

Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(
s->s.substring(0, 1), //表示首字母相同的String分到一组
Collectors.toList() //表示输出为List
));

System.out.println(groups);

其他操作

  • 排序 sorted()
    • 要求Stream的每个元素必须实现Comparable接口
    • 自定义排序,需指定Comparator,如sorted(String::compareToIgnoreCase)
    • sorted()是转换操作,返回新的Stream
  • 去重 distinct()
  • 截取 用于将无限转化为有限的Stream
    • skip()跳过当前Stream的前N个元素
    • limit()用于截取当前Stream最多前N个元素
  • 合并 concat()
  • flatMap() 把Stream的每个元素映射为Stream,然后合并成一个新的Stream
1
2
3
4
5
6
7
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));

//将Stream转化为Stream<Integer>
Stream<Integer> i = s.flatMap(list -> list.stream());
  • 并行处理 parallel()
1
2
3
4
Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
.sorted() // 可以进行并行排序
.toArray(String[]::new);
  • 其他聚合方法

    • count() 返回元素个数max(Compareator<? super T> cp)找出最大
    • 针对IntStreamLongStreamDoubleStream聚合方法
      • sum() 求和
      • average() 求平均数
    • 用来测试元素是否满足条件
      • boolean allmatch(Predicate<? super T>)所有元素是否满足
      • boolean anyMatch(Predicate<? super T>)是否至少有一个满足
    • forEach(),循环打印
CATALOG
  1. 1. 函数式编程
    1. 1.0.1. Lambda表达式
    2. 1.0.2. 方法引用
      1. 1.0.2.1. 构造方法引用
    3. 1.0.3. 使用Stream
      1. 1.0.3.1. 创建Stream
    4. 1.0.4. 使用map
    5. 1.0.5. 使用filter
    6. 1.0.6. 使用reduce
    7. 1.0.7. 输出
      1. 1.0.7.1. 输出为List
      2. 1.0.7.2. 输出为数组
      3. 1.0.7.3. 输出为Map
      4. 1.0.7.4. 分组输出
    8. 1.0.8. 其他操作