参考:<-------------------------------------廖雪峰学Java-------------------------------------------->
1. 函数式编程基本概念
- 函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为Lambda计算
- 计算机(Computer)和计算(Compute)的概念:计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令;计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远;
对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言 - 函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数 - Java不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带this参数的函数
- Java平台从Java 8开始,支持函数式编程(引入lambda表达式)
2. Lambda基础
- 函数式编程(Functional Programming)把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式
- 从Java 8开始,我们可以用Lambda表达式替换单方法接口:
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
System.out.println(String.join(", ", array));
}
}
- 什么是单方法接口,这里以
Comparator<T>接口 为例:
@FunctionalInterface
public interface Comparator<T> {
......
}
首先需要说明的是 @FunctionalInterface注解:(文档) An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract. If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface’s abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere. 总结包括三个要点:
- 由FunctionalInterface包裹的接口只有一个 “ 抽象方法 ”(单方法接口)
- 定义在接口中的default方法不算做 “ 抽象方法 ”
- 重写java.lang.Objects中的方法而形成的抽象方法,不算做“抽象方法”,因总是为会有来自Object或者其他类中的实现
- 观察Lambda表达式的写法,它只需要写出方法定义:
(s1, s2) -> {
return s1.compareTo(s2);
}
参数是(s1, s2),参数类型可以省略,因为编译器可以自动推断出String类型。-> { ... }表示方法体,所有代码写在内部即可。Lambda表达式没有class定义,因此写法非常简洁
- 如果只有一行return xxx的代码,完全可以用更简单的写法:
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
返回值的类型也是由编译器自动推断的,这里推断出的返回值是int,因此,只要返回int,编译器就不会报错
3. 方法引用
- 解决上面的问题,除了Lambda表达式,我们还可以直接传入方法引用:
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, Main::cmp);
System.out.println(String.join(", ", array));
}
static int cmp(String s1, String s2) {
return s1.compareTo(s2);
}
}
所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用(除了方法名不相同之外,方法参数以及方法返回值都相同)
- 注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系
- 引用实例方法:
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, String::compareTo);
System.out.println(String.join(", ", array));
}
}
这里String::compareTo的定义为:
public final class String {
public int compareTo(String o) {
...
}
}
明明只接受了一个String参数为什么却可以编译通过并且顺利执行呢?这是因为实例方法隐含了一个this参数
- 除了引用静态方法以及实例方法,还可以对构造方法进行引用,例如可以在Stream的map处理过程中传入构造方法的引用(关于Stream的更详细信息可以参考下一小节)
4. 使用Stream
- Java8不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中
- Stream的特点总结:
1. 不同于java.io的InputStream和OutputStream,Stream代表的是任意Java对象的序列 2. Stream可以存储有限个或者“无限个”元素,因为Stream并不是将所有元素都真实地保存在内存中,Stream可以通过实时计算来得到相应的结果 3. Stream实现了惰性计算;可以将对Stream的操作分为两个部分:一、逻辑算子;二、执行算子;在添加逻辑算子的过程中,Stream并不会直接对表示的各个元素进行相应的运算,而是存储计算的逻辑,只有当执行算子需求当前元素的最终结果时才会依据存储的计算逻辑,实时地计算得到相应的结果并交还 4. Stream API支持函数式编程和链式操作 具体实现方式,下面介绍
4.1 创建Stream
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("A", "B", "C", "D");
stream.forEach(System.out::println);
}
}
这种方式没有办法体现Stream的强大,适用于测试情景
public class Main {
public static void main(String[] args) {
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
Stream<String> stream2 = List.of("X", "Y", "Z").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
}
}
前两种方法创造出来的元素值都是固定的
- 方式三:基于Supplier,创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象:
Stream<String> s = Stream.generate(Supplier<String> sp);
基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列 示例:自然数序列
public class Main {
public static void main(String[] args) {
Stream<Integer> natual = Stream.generate(new NatualSupplier());
natual.limit(20).forEach(System.out::println);
}
}
class NatualSupplier implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
对于无限序列,如果直接调用forEach()或者count()这些最终求值操作,会进入死循环,因为永远无法计算完这个序列,所以正确的方法是先把无限序列变成有限序列,例如,用limit()方法可以截取前面若干个元素,这样就变成了一个有限序列
- 其他方式:
- 基于一些API接口:例如Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
...
}
- 基本类型:因为Java的范型不支持基本类型,所以我们无法用Stream这样的类型,会发生编译错误。为了保存int,只能使用Stream,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream、LongStream和DoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
4.2 使用map方法
- Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream
- map操作,把一个Stream的每个元素一一对应到应用了目标函数的结果上:
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);
- map()方法接收的对象是Function接口对象,它定义了一个**apply()**方法,负责把一个T类型转换成R类型:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Function的定义为:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
因此我们可以直接传入Lambda表达式来代替匿名类
public class Main {
public static void main(String[] args) {
List.of(" Apple ", " pear ", " ORANGE", " BaNaNa ")
.stream()
.map(String::trim)
.map(String::toLowerCase)
.forEach(System.out::println);
}
}
4.3 使用filter
- filter() 操作是对一个Stream的所有元素一一进行测试,不满足条件的元素就会被“过滤”,剩下的满足条件的元素则组成一个新的Stream
- 示例:过滤偶数
public class Main {
public static void main(String[] args) {
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(n -> n % 2 != 0)
.forEach(System.out::println);
}
}
- filter()方法接收的对象是Predicate接口对象,它定义了一个**test()**方法,负责判断元素是否符合条件:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
用Lambda表达式替代
- filter()除了常用于数值外,也可应用于任何Java对象,过滤规则需要自己定义
4.4 使用reduce
- Stream.reduce() Stream的一个聚合方法(执行方法),它可以把一个Stream的所有元素按照聚合函数聚合成一个结果
- 示例:累加
public class Main {
public static void main(String[] args) {
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
System.out.println(sum);
}
}
可以解析为:
// 计算过程:
acc = 0 // 初始化为指定值
acc = acc + n = 0 + 1 = 1 // n = 1
acc = acc + n = 1 + 2 = 3 // n = 2
acc = acc + n = 3 + 3 = 6 // n = 3
acc = acc + n = 6 + 4 = 10 // n = 4
acc = acc + n = 10 + 5 = 15 // n = 5
acc = acc + n = 15 + 6 = 21 // n = 6
acc = acc + n = 21 + 7 = 28 // n = 7
acc = acc + n = 28 + 8 = 36 // n = 8
acc = acc + n = 36 + 9 = 45 // n = 9
实际上reduce()操作是一个求和操作
- reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:
@FunctionalInterface
public interface BinaryOperator<T> {
T apply(T t, T u);
}
- 如果去掉初始值,我们会得到一个Optional<Integer>:
Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent()) {
System.out.println(opt.get());
}
Stream的元素有可能是0个,Optional对象用于进一步判断结果是否存在
public class Main {
public static void main(String[] args) {
List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
Map<String, String> map = props.stream()
.map(kv -> {
String[] ss = kv.split("\\=", 2);
return Map.of(ss[0], ss[1]);
})
.reduce(new HashMap<String, String>(), (m, kv) -> {
m.putAll(kv);
return m;
});
map.forEach((k, v) -> {
System.out.println(k + " = " + v);
});
}
}
4.5 输出集合
- reduce()只是一种聚合操作,如果我们希望把Stream的元素保存到集合,例如List,因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素
- 输出为List:
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);
}
}
调用collect()并传入Collectors.toList()对象,它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
s -> s.substring(0, s.indexOf(':')),
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
}
public class Main {
public static void main(String[] args) {
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), Collectors.toList()));
System.out.println(groups);
}
}
Collectors.groupingBy()需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组;第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List
4.6 其他操作
- 对Stream的元素进行排序十分简单,只需调用sorted()方法:
public class Main {
public static void main(String[] args) {
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(list);
}
}
此方法要求Stream的每个元素必须实现Comparable接口。如果要自定义排序,在sourted方法中传入指定的Comparator即可
- 对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct():
List.of("A", "B", "A", "C", "B", "D")
.stream()
.distinct()
.collect(Collectors.toList());
- 截取操作常用于把一个无限的Stream转换成有限的Stream,skip()用于跳过当前Stream的前N个元素,limit()用于截取当前Stream最多前N个元素:
List.of("A", "B", "C", "D", "E", "F")
.stream()
.skip(2)
.limit(3)
.collect(Collectors.toList());
- 将两个Stream合并 为一个Stream可以使用Stream的静态方法concat():
Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList()));
- 扁平化操作:如果Stream的元素是集合Stream<List<Integer>>,而我们希望把上述Stream转换为Stream<Integer>,就可以使用flatMap():
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));
Stream<Integer> i = s.flatMap(list -> list.stream());
flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream
- 把一个普通Stream转换为可以并行处理的Stream只需要用parallel()进行转换:
Stream<String> s = ...
String[] result = s.parallel()
.sorted()
.toArray(String[]::new);
经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升
- 除了reduce()和collect()外,Stream还有一些常用的聚合方法:
1. count():用于返回元素个数;
2. max(Comparator<? super T> cp):找出最大元素;
3. min(Comparator<? super T> cp):找出最小元素。
- 针对IntStream、LongStream和DoubleStream,还额外提供了以下聚合方法:
1. sum():对所有元素求和;
2. average():对所有元素求平均数。
1. boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
2. boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。
- forEach()方法可以循环处理Stream的每个元素,我们经常传入System.out::println来打印Stream的元素
|