函数式编程(Functional Programming)是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式。
Lambda表达式
在Java程序中,我们经常遇到一大堆单方法接口,即一个接口只定义了一个方法:
- Comparator
- Runnable
- Callable
以Comparator
为例,我们想要调用Arrays.sort()
时,可以传入一个Comparator
实例,以匿名类方式编写如下:
String[] array = ...
Arrays.sort(array, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
上述写法非常繁琐。从Java 8开始,我们可以用Lambda表达式替换单方法接口。改写上述代码如下:// Lambda import java.util.Arrays;
Run
观察Lambda表达式的写法,它只需要写出方法定义:
(s1, s2) -> {
return s1.compareTo(s2);
}
其中,参数是(s1, s2)
,参数类型可以省略,因为编译器可以自动推断出String
类型。-> { ... }
表示方法体,所有代码写在内部即可。Lambda表达式没有class
定义,因此写法非常简洁。
如果只有一行return xxx
的代码,完全可以用更简单的写法:
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
返回值的类型也是由编译器自动推断的,这里推断出的返回值是int
,因此,只要返回int
,编译器就不会报错。
FunctionalInterface
我们把只定义了单方法的接口称之为FunctionalInterface
,用注解@FunctionalInterface
标记。例如,Callable
接口:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
再来看Comparator
接口:
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
...
}
...
}
虽然Comparator
接口有很多方法,但只有一个抽象方法int compare(T o1, T o2)
,其他的方法都是default
方法或static
方法。另外注意到boolean equals(Object obj)
是Object
定义的方法,不算在接口方法内。因此,Comparator
也是一个FunctionalInterface
。
方法引用
实际上,除了Lambda表达式,我们还可以直接传入方法引用。例如:
import java.util.Arrays;
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);
}
}
上述代码在Arrays.sort()
中直接传入了静态方法cmp
的引用,用Main::cmp
表示。
因此,所谓方法引用,是指如果某个方法签名和接口恰好一致,就可以直接传入方法引用。
因为Comparator
接口定义的方法是int compare(String, String)
,和静态方法int cmp(String, String)
相比,除了方法名外,方法参数一致,返回类型相同,因此,我们说两者的方法签名一致,可以直接把方法名作为Lambda表达式传入:
Arrays.sort(array, Main::cmp);
注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。
引用实例方法
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()
方法也符合Lambda定义。
观察String.compareTo()
的方法定义:
public final class String {
public int compareTo(String o) {
...
}
}
这个方法的签名只有一个参数,为什么和int Comparator.compare(String, String)
能匹配呢?
因为实例方法有一个隐含的this
参数,String
类的compareTo()
方法在实际调用的时候,第一个隐含参数总是传入this
,相当于静态方法:
public static int compareTo(this, String o);
所以,String.compareTo()
方法也可作为方法引用传入。
使用Stream
Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream
包中。
划重点:这个Stream
不同于java.io
的InputStream
和OutputStream
,它代表的是任意Java对象的序列。两者对比如下:
|
| java.io | java.util.stream | | —- | —- | —- |
|
存储
| 顺序读写的byte
或char
| 顺序输出的任意Java对象实例
|
| 用途 | 序列化至文件或网络 | 内存计算/业务逻辑 |
有同学会问:一个顺序输出的Java对象序列,不就是一个List
容器吗?
再次划重点:这个Stream
和List
也不一样,List
存储的每个元素都是已经存储在内存中的某个Java对象,而Stream
输出的元素可能并没有预先存储在内存中,而是实时计算出来的。
换句话说,List
的用途是操作一组已存在的Java对象,而Stream
实现的是惰性计算,两者对比如下:
|
| java.util.List | java.util.stream | | —- | —- | —- |
| 元素 | 已分配并存储在内存 | 可能未分配,实时计算 |
| 用途 | 操作一组已存在的Java对象 | 惰性计算 |
Stream
看上去有点不好理解,但我们举个例子就明白了。
如果我们要表示一个全体自然数的集合,显然,用List
是不可能写出来的,因为自然数是无限的,内存再大也没法放到List
中:
List<BigInteger> list = ??? // 全体自然数?
但是,用Stream
可以做到。写法如下:
Stream<BigInteger> naturals = createNaturalStream(); // 全体自然数
我们先不考虑createNaturalStream()
这个方法是如何实现的,我们看看如何使用这个Stream
。
首先,我们可以对每个自然数做一个平方,这样我们就把这个Stream
转换成了另一个Stream
:
Stream<BigInteger> naturals = createNaturalStream(); // 全体自然数
Stream<BigInteger> streamNxN = naturals.map(n -> n.multiply(n)); // 全体自然数的平方
因为这个streamNxN
也有无限多个元素,要打印它,必须首先把无限多个元素变成有限个元素,可以用limit()
方法截取前100个元素,最后用forEach()
处理每个元素,这样,我们就打印出了前100个自然数的平方:
Stream<BigInteger> naturals = createNaturalStream();
naturals.map(n -> n.multiply(n)) // 1, 4, 9, 16, 25...
.limit(100)
.forEach(System.out::println);
我们总结一下Stream
的特点:它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。Stream
的另一个特点是,一个Stream
可以轻易地转换为另一个Stream
,而不是修改原Stream
本身。
最后,真正的计算通常发生在最后结果的获取,也就是惰性计算。
Stream<BigInteger> naturals = createNaturalStream(); // 不计算
Stream<BigInteger> s2 = naturals.map(BigInteger::multiply); // 不计算
Stream<BigInteger> s3 = s2.limit(100); // 不计算
s3.forEach(System.out::println); // 计算
惰性计算的特点是:一个Stream
转换为另一个Stream
时,实际上只存储了转换规则,并没有任何计算发生。
例如,创建一个全体自然数的Stream
,不会进行计算,把它转换为上述s2
这个Stream
,也不会进行计算。再把s2
这个无限Stream
转换为s3
这个有限的Stream
,也不会进行计算。只有最后,调用forEach
确实需要Stream
输出的元素时,才进行计算。我们通常把Stream
的操作写成链式操作,代码更简洁:
createNaturalStream()
.map(BigInteger::multiply)
.limit(100)
.forEach(System.out::println);
因此,Stream API的基本用法就是:创建一个Stream
,然后做若干次转换,最后调用一个求值方法获取真正计算的结果:
int result = createNaturalStream() // 创建Stream
.filter(n -> n % 2 == 0) // 任意个转换
.map(n -> n * n) // 任意个转换
.limit(100) // 任意个转换
.sum(); // 最终计算结果
Stream的创建
基于数组或Collection
第二种创建Stream
的方法是基于一个数组或者Collection
,这样该Stream
输出的元素就是数组或者Collection
持有的元素:
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);
}
}
把数组变成Stream
使用Arrays.stream()
方法。对于Collection
(List
、Set
、Queue
等),直接调用stream()
方法就可以获得Stream
。
基于Supplier
创建Stream
还可以通过Stream.generate()
方法,它需要传入一个Supplier
对象:
基于Supplier
创建的Stream
会不断调用Supplier.get()
方法来不断产生下一个元素,这种Stream
保存的不是元素,而是算法,它可以用来表示无限序列。
例如,我们编写一个能不断生成自然数的Supplier
,它的代码非常简单,每次调用get()
方法,就生成下一个自然数:
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;
}
}
上述代码我们用一个Supplier
模拟了一个无限序列(当然受int
范围限制不是真的无限大)。如果用List
表示,即便在int
范围内,也会占用巨大的内存,而Stream
几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。
对于无限序列,如果直接调用forEach()
或者count()
这些最终求值操作,会进入死循环,因为永远无法计算完这个序列,所以正确的方法是先把无限序列变成有限序列,例如,用limit()
方法可以截取前面若干个元素,这样就变成了一个有限序列,对这个有限序列调用forEach()
或者count()
操作就没有问题。
其他方法
创建Stream
的第三种方法是通过一些API提供的接口,直接获得Stream
。
例如,Files
类的lines()
方法可以把一个文件变成一个Stream
,每个元素代表文件的一行内容:
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
...
}
此方法对于按行遍历文本文件十分有用。
另外,正则表达式的Pattern
对象有一个splitAsStream()
方法,可以直接把一个长字符串分割成Stream
序列而不是数组:
Pattern p = Pattern.compile("\\s+");
Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");
s.forEach(System.out::println);
基本类型
因为Java的范型不支持基本类型,所以我们无法用Stream
这样的类型,会发生编译错误。为了保存int
,只能使用String
,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream
、LongStream
和DoubleStream
这三种使用基本类型的Stream
,它们的使用方法和范型Stream
没有大的区别,设计这三个Stream
的目的是提高运行效率:
// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
使用Map
Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换成另一个Stream。
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);
如果我们查看Stream
的源码,会发现map()
方法接收的对象是Function
接口对象,它定义了一个apply()
方法,负责把一个T
类型转换成R
类型:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
其中,Function
的定义是:
@FunctionalInterface
public interface Function<T, R> {
// 将T类型转换为R:
R apply(T t);
}
利用map()
,不但能完成数学计算,对于字符串操作,以及任何Java对象都是非常有用的。例如:
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); // 打印
}
}
filter
filter()即对Stream的所有元素一一测试,不满足条件的被过滤掉,剩下的元素构成了一个新的Stream。下例为过滤掉奇数的filter()
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> {
// 判断元素t是否符合条件:
boolean test(T t);
}
filter()
除了常用于数值外,也可应用于任何Java对象。例如,从一组给定的LocalDate
中过滤掉工作日,以便得到休息日:
public class Main {
public static void main(String[] args) {
Stream.generate(new LocalDateSupplier())
.limit(31)
.filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
.forEach(System.out::println);
}
}
class LocalDateSupplier implements Supplier<LocalDate> {
LocalDate start = LocalDate.of(2020, 1, 1);
int n = -1;
public LocalDate get() {
n++;
return start.plusDays(n);
}
}
reduce
map()
和filter()
都是Stream
的转换方法,而Stream.reduce()
则是Stream
的一个聚合方法,它可以把一个Stream
的所有元素按照聚合函数聚合成一个结果,聚合操作和filter和map操作不同,需要实际计算发生。
我们来看一个简单的聚合方法:
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); // 45
}
}
除了可以对数值进行累积计算外,灵活运用reduce()
也可以对Java对象进行操作。下面的代码演示了如何将配置文件的每一行配置通过map()
和reduce()
操作聚合成一个Map
:
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()
// 把k=v转换为Map[k]=v:
.map(kv -> {
String[] ss = kv.split("\\=", 2);
return Map.of(ss[0], ss[1]);
})
// 把所有Map聚合到一个Map:
.reduce(new HashMap<String, String>(), (m, kv) -> {
m.putAll(kv);
return m;
});
// 打印结果:
map.forEach((k, v) -> {
System.out.println(k + " = " + v);
});
}
}
Stream输出
输出为集合
reduce()
只是一种聚合操作,如果我们希望把Stream
的元素保存到集合,例如List
,因为List
的元素是确定的Java对象,因此,把Stream
变为List
不是一个转换操作,而是一个聚合操作,它会强制Stream
输出每个元素。
下面的代码演示了如何将一组String
先过滤掉空字符串,然后把非空字符串保存到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);
}
}
输出为数组
把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()
方法,并传入数组的“构造方法”:
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
注意到传入的“构造方法”是String[]::new
,它的签名实际上是IntFunction
定义的String[] apply(int)
,即传入int
参数,获得String[]
数组的返回值。
输出为Map
如果我们要把Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:
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映射为key:
s -> s.substring(0, s.indexOf(':')),
// 把元素s映射为value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
}
Stream其他操作
对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
接口。如果要自定义排序,传入指定的Comparator
即可:
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted(String::compareToIgnoreCase)
.collect(Collectors.toList());
注意sorted()
只是一个转换操作,它会返回一个新的Stream
。
去重
对一个Stream
的元素进行去重,没必要先转换为Set
,可以直接用distinct()
:
List.of("A", "B", "A", "C", "B", "D")
.stream()
.distinct()
.collect(Collectors.toList()); // [A, B, C, D]
跳过
截取操作常用于把一个无限的Stream
转换成有限的Stream
,skip()
用于跳过当前Stream
的前N个元素,limit()
用于截取当前Stream
最多前N个元素:
List.of("A", "B", "C", "D", "E", "F")
.stream()
.skip(2) // 跳过A, B
.limit(3) // 截取C, D, E
.collect(Collectors.toList()); // [C, D, E]
截取操作也是一个转换操作,将返回新的Stream
。
合并
将两个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())); // [A, B, C, D, E]
其他聚合方法
除了reduce()
和collect()
外,Stream
还有一些常用的聚合方法:
count()
:用于返回元素个数;max(Comparator cp)
:找出最大元素;min(Comparator cp)
:找出最小元素。
针对IntStream
、LongStream
和DoubleStream
,还额外提供了以下聚合方法:
sum()
:对所有元素求和;average()
:对所有元素求平均数。
还有一些方法,用来测试Stream
的元素是否满足以下条件:
boolean allMatch(Predicate)
:测试是否所有元素均满足测试条件;boolean anyMatch(Predicate)
:测试是否至少有一个元素满足测试条件。
最后一个常用的方法是forEach()
,它可以循环处理Stream
的每个元素,我们经常传入System.out::println
来打印Stream
的元素:
Stream<String> s = ...
s.forEach(str -> {
System.out.println("Hello, " + str);
});