本博客是函数式编程这一节的学习笔记,网址:https://www.liaoxuefeng.com/wiki/1252599548343744/1255943847278976
这一节课内容分为3个主题:Lambda基础、方法引用和试用Stream。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!
函数式编程最早是数学家阿隆佐·邱奇研究的一套函数变换逻辑,又称Lambda Calculus(λ-Calculus),所以也经常把函数式编程称为Lambda计算。
Java平台从Java 8开始,支持函数式编程。
1.Lambda基础
关于Lambda的详细用法介绍强烈推荐此博客:https://blog.youkuaiyun.com/bitcarmanlee/article/details/70195403。题外话,此博客作者是一大牛,java分类中的文章值得细看。
1.1.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;
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));
}
}
观察Lambda表达式的写法,它只需要写出方法定义:
(s1, s2) -> {
return s1.compareTo(s2);
}
其中,参数是(s1, s2),参数类型可以省略,因为编译器可以自动推断出String类型。-> { … }表示方法体,所有代码写在内部即可。Lambda表达式没有class定义,因此写法非常简洁。
如果只有一行return xxx的代码,完全可以用更简单的写法:
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
返回值的类型也是由编译器自动推断的,这里推断出的返回值是int,因此,只要返回int,编译器就不会报错。
有关Comparator用法,贴一个链接:
[1]https://blog.youkuaiyun.com/u012250875/article/details/55126531
下面是java中一些使用到Comparator接口的地方:
Arrays.sort(T[], Comparator<? super T> c);
Collections.sort(List<T> list, Comparator<? super T> c);
在利用Comparator接口时,需要重写
int compare(T o1, T o2);
这个函数,关于此函数的比较顺序,jdk官方默认是升序,是基于:
int compare(T o1, T o2)
Returns: a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
- o1 < o2: 返回负整数,标识o1和o2的顺序不用交换
- o1 = o2: 返回零,o1和o2的位置不用动
- o1 > o2: 返回正整数,o1和o2的位置需要交换
若要实现降序排列,则只需要
- o1 > o2: 返回负整数,标识o1和o2的顺序不用交换
- o1 = o2: 返回零,o1和o2的位置不用动
- o1 < o2: 返回正整数,o1和o2的位置需要交换
有关compareTo的用法,贴两个链接:
[1]https://www.runoob.com/java/number-compareto.html
[2]https://www.cnblogs.com/efforts-will-be-lucky/p/7052910.html
关于匿名内部类解析:
[1]https://blog.youkuaiyun.com/chengqiuming/article/details/91352913
1.2.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。
1.3.小结
- 单方法接口被称为FunctionalInterface。
- 接收FunctionalInterface作为参数的时候,可以把实例化的匿名类改写为Lambda表达式,能大大简化代码。
- Lambda表达式的参数和返回值均可由编译器自动推断。
2.方法引用
2.1.静态方法引用/实例方法引用
使用Lambda表达式,我们就可以不必编写FunctionalInterface接口的实现类,从而简化代码:
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
实际上,除了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< String >接口定义的方法是int compare(String, String),和静态方法int cmp(String, String)相比,除了方法名外,方法参数一致,返回类型相同,因此,我们说两者的方法签名一致,可以直接把方法名作为Lambda表达式传入:
Arrays.sort(array, Main::cmp);
注意:在这里,方法签名只看参数类型和返回类型,不看方法名称,也不看类的继承关系。
我们再看看如何引用实例方法。如果我们把代码改写如下:
import java.util.Arrays;
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< String >.compare(String, String)能匹配呢?
因为实例方法有一个隐含的this参数,String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:
public static int compareTo(this, String o);
所以,String.compareTo()方法也可作为方法引用传入。
关于静态方法和实例方法的说明参考:
[1]https://www.cnblogs.com/gemuxiaoshe/p/10641670.html
2.2.构造方法引用
除了可以引用静态方法和实例方法,我们还可以引用构造方法。
我们来看一个例子:如果要把一个List< String >转换为List< Person >,应该怎么办?
class Person {
String name;
public Person(String name) {
this.name = name;
}
}
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = ???
传统的做法是先定义一个ArrayList< Person >,然后用for循环填充这个List:
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = new ArrayList<>();
for (String name : names) {
persons.add(new Person(name));
}
要更简单地实现String到Person的转换,我们可以引用Person的构造方法:
// 引用构造方法
import java.util.*;
import java.util.stream.*;
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;
}
}
后面我们会讲到Stream的map()方法。现在我们看到,这里的map()需要传入的FunctionalInterface的定义是:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
把泛型对应上就是方法签名Person apply(String),即传入参数String,返回类型Person。而Person类的构造方法恰好满足这个条件,因为构造方法的参数是String,而构造方法虽然没有return语句,但它会隐式地返回this实例,类型就是Person,因此,此处可以引用构造方法。构造方法的引用写法是类名::new,因此,此处传入Person::new。
2.3.小结
FunctionalInterface允许传入:
- 接口的实现类(传统写法,代码较繁琐);
- Lambda表达式(只需列出参数名,由编译器推断类型);
- 符合方法签名的静态方法;
- 符合方法签名的实例方法(实例类型被看做第一个参数类型);
- 符合方法签名的构造方法(实例类型被看做返回类型)。
FunctionalInterface不强制继承关系,不需要方法名称相同,只要求方法参数(类型和数量)与方法返回类型相同,即认为方法签名相同。
3.使用Stream
3.1.背景介绍
Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中。
划重点:这个Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java对象的序列。两者对比如下:
有同学会问:一个顺序输出的Java对象序列,不就是一个List容器吗?
再次划重点:这个Stream和List也不一样,List存储的每个元素都是已经存储在内存中的某个Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是实时计算出来的。
换句话说,List的用途是操作一组已存在的Java对象,而Stream实现的是惰性计算,两者对比如下:
我们总结一下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 API的特点是:
- Stream API提供了一套新的流式处理的抽象序列;
- Stream API支持函数式编程和链式操作;
- Stream可以表示无限序列,并且大多数情况下是惰性求值的。
3.2.创建Stream
要使用Stream,就必须现创建它。创建Stream有很多种方法,我们来一一介绍。
3.2.1.Stream.of()
创建Stream最简单的方式是直接用Stream.of()静态方法,传入可变参数即创建了一个能输出确定元素的Stream:
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("A", "B", "C", "D");
// forEach()方法相当于内部循环调用,
// 可传入符合Consumer接口的void accept(T t)的方法引用:
stream.forEach(System.out::println);
}
}
虽然这种方式基本上没啥实质性用途,但测试的时候很方便。
3.2.2.基于数组或Collection
第二种创建Stream的方法是基于一个数组或者Collection,这样该Stream输出的元素就是数组或者Collection持有的元素:
import java.util.*;
import java.util.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);
}
}
- 把数组变成Stream使用Arrays.strem()方法;
- 对于Collection(List、Set、Queue等),直接调用stream()方法就可以获得Stream;
- 上述创建Stream的方法都是把一个现有的序列变为Stream,它的元素是固定的。
3.2.3.基于Supplier
具体请参考网址:https://www.liaoxuefeng.com/wiki/1252599548343744/1322655160467490
3.2.4.其他方法
创建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);
3.2.5.基本类型
因为Java的范型不支持基本类型,所以我们无法用Stream< int >这样的类型,会发生编译错误。为了保存int,只能使用String< Integer >,但这样会产生频繁的装箱、拆箱操作。为了提高效率,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);
3.2.6.小结
创建Stream的方法有 :
- 通过指定元素、指定数组、指定Collection创建Stream;
- 通过Supplier创建Stream,可以是无限序列;
- 通过其他类的相关方法创建。
基本类型的Stream有IntStream、LongStream和DoubleStream。
3.3.使用map
Stream.map()是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream。
所谓map操作,就是把一种操作运算,映射到一个序列的每一个元素上。例如,对x计算它的平方,可以使用函数f(x) = x * x。我们把这个函数映射到一个序列1,2,3,4,5上,就得到了另一个序列1,4,9,16,25:
可见,map操作,把一个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对象都是非常有用的。例如:
import java.util.*;
import java.util.stream.*;
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); // 打印
}
}
小结:
map()方法用于将一个Stream的每个元素映射成另一个元素并转换成一个新的Stream;
可以将一种元素类型转换成另一种元素类型。
3.4.使用filter
Stream.filter()是Stream的另一个常用转换方法。
所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream。
例如,我们对1,2,3,4,5这个Stream调用filter(),传入的测试函数f(x) = x % 2 != 0用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列1,3,5:
用IntStream写出上述逻辑,代码如下:
import java.util.stream.IntStream;
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()后生成的Stream元素可能变少。
filter()方法接收的对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件:
@FunctionalInterface
public interface Predicate<T> {
// 判断元素t是否符合条件:
boolean test(T t);
}
filter()除了常用于数值外,也可应用于任何Java对象。例如,从一组给定的LocalDate中过滤掉工作日,以便得到休息日:
import java.time.*;
import java.util.function.*;
import java.util.stream.*;
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);
}
}
小结:
使用filter()方法可以对一个Stream的每个元素进行测试,通过测试的元素被过滤后生成一个新的Stream。
3.5.使用reduce
map()和filter()都是Stream的转换方法,而Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。
我们来看一个简单的聚合方法:
import java.util.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); // 45
}
}
reduce()方法传入的对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:
@FunctionalInterface
public interface BinaryOperator<T> {
// Bi操作:两个输入,一个输出
T apply(T t, T u);
}
上述代码看上去不好理解,但我们用for循环改写一下,就容易理解了:
Stream<Integer> stream = ...
int sum = 0;
for (n : stream) {
sum = (sum, n) -> sum + n;
}
可见,reduce()操作首先初始化结果为指定值(这里是0),紧接着,reduce()对每个元素依次调用(acc, n) -> acc + n,其中,acc是上次计算的结果:
// 计算过程:
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()操作是一个求和。
如果去掉初始值,我们会得到一个Optional< Integer >:
Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent) {
System.out.println(opt.get());
}
这是因为Stream的元素有可能是0个,这样就没法调用reduce()的聚合函数了,因此返回Optional对象,需要进一步判断结果是否存在。
利用reduce(),我们可以把求和改成求积,代码也十分简单:
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
int s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(1, (acc, n) -> acc * n);
System.out.println(s); // 362880
}
}
注意:计算求积时,初始值必须设置为1。
除了可以对数值进行累积计算外,灵活运用reduce()也可以对Java对象进行操作。下面的代码演示了如何将配置文件的每一行配置通过map()和reduce()操作聚合成一个Map< String, String >:
import java.util.*;
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);
});
}
}
关于Map.putAll()方法,该方法用来追加另一个Map对象到当前Map集合对象,它会把另一个Map集合对象中的所有内容添加到当前Map集合对象。
putAll(Map<? extends K,? extends V> m)
小结:
- reduce()方法将一个Stream的每个元素依次作用于BinaryOperator,并将结果合并;
- reduce()是聚合方法,聚合方法会立刻对Stream进行计算。
3.6.输出集合
3.6.1.转换操作/聚合操作
我们介绍了Stream的几个常见操作:map()、filter()、reduce()。这些操作对Stream来说可以分为两类,一类是转换操作,即把一个Stream转换为另一个Stream,例如map()和filter(),另一类是聚合操作,即对Stream的每个元素进行计算,得到一个确定的结果,例如reduce()。
区分这两种操作是非常重要的,因为对于Stream来说,对其进行转换操作并不会触发任何计算!我们可以做个实验:
import java.util.function.Supplier;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
System.out.println(s3); // java.util.stream.ReferencePipeline$3@49476842
}
}
class NatualSupplier implements Supplier<Long> {
long n = 0;
public Long get() {
n++;
return n;
}
}
因为s1是一个Long类型的序列,它的元素高达922亿个,但执行上述代码,既不会有任何内存增长,也不会有任何计算,因为转换操作只是保存了转换规则,无论我们对一个Stream转换多少次,都不会有任何实际计算发生。
而聚合操作则不一样,聚合操作会立刻促使Stream输出它的每一个元素,并依次纳入计算,以获得最终结果。所以,对一个Stream进行聚合操作,会触发一系列连锁反应:
Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
Stream<Long> s4 = s3.limit(10);
s4.reduce(0, (acc, n) -> acc + n);
我们对s4进行reduce()聚合计算,会不断请求s4输出它的每一个元素。因为s4的上游是s3,它又会向s3请求元素,导致s3向s2请求元素,s2向s1请求元素,最终,s1从Supplier实例中请求到真正的元素,并经过一系列转换,最终被reduce()聚合出结果。
可见,聚合操作是真正需要从Stream请求数据的,对一个Stream做聚合计算后,结果就不是一个Stream,而是一个其他的Java对象。
3.6.2.输出为List/Set
reduce()只是一种聚合操作,如果我们希望把Stream的元素保存到集合,例如List,因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素。
下面的代码演示了如何将一组String先过滤到空字符串,然后把非空字符串保存到List中:
import java.util.*;
import java.util.stream.*;
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的方法是调用collect()并传入Collectors.toList()对象,它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。
类似的,collect(Collectors.toSet())可以把Stream的每个元素收集到Set中。
3.6.3.输出为数组
把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”:
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
注意到传入的“构造方法”是String[]::new,它的签名实际上是IntFunction<String[]>定义的String[] apply(int),即传入int参数,获得String[]数组的返回值。
3.6.4.输出为Map
如果我们要把Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:
import java.util.*;
import java.util.stream.*;
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);
}
}
3.6.5.分组输出
Stream还有一个强大的分组功能,可以按组输出。我们看下面的例子:
import java.util.*;
import java.util.stream.*;
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,上述代码运行结果如下:
{
A=[Apple, Avocado, Apricots],
B=[Banana, Blackberry],
C=[Coconut, Cherry]
}
可见,结果一共有3组,按"A",“B”,"C"分组,每一组都是一个List。
假设有这样一个Student类,包含学生姓名、班级和成绩:
class Student {
int gradeId; // 年级
int classId; // 班级
String name; // 名字
int score; // 分数
}
如果我们有一个Stream,利用分组输出,可以非常简单地按年级或班级把Student归类。
3.6.6.小结
- Stream可以输出为集合:
- Stream通过collect()方法可以方便地输出为List、Set、Map,还可以分组输出。
3.7.其他操作
我们把Stream提供的操作分为两类:转换操作和聚合操作。除了前面介绍的常用操作外,Stream还提供了一系列非常有用的方法。
3.7.1.排序
对Stream的元素进行排序十分简单,只需调用sorted()方法:
import java.util.*;
import java.util.stream.*;
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());
关于sort()用法的一个例子
package lambda;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class lambdaSortTest {
public static void main(String[] args) {
List<Dog> dogList = new ArrayList<>();
Dog dog1 = new Dog(13, "black");
Dog dog2 = new Dog(13, "red");
Dog dog3 = new Dog(8, "yellow");
dogList.add(dog1);
dogList.add(dog2);
dogList.add(dog3);
dogList.stream().sorted(Comparator
.comparing(Dog::getPrice) // 先按照价格升序排列
.thenComparing(Dog::getColor)) // 再按照颜色升序排列
.forEach(System.out::println);
}
}
class Dog {
private int price;
private String color;
public Dog(int price, String color) {
this.price = price;
this.color = color;
}
public int getPrice() {
return price;
}
public String getColor() {
return color;
}
@Override
public String toString() {
return "Dog, price = "
+ price + "; color = "
+ color + ".";
}
}
输出如下:
Dog, price = 8; color = yellow.
Dog, price = 13; color = black.
Dog, price = 13; color = red.
注意sorted()只是一个转换操作,它会返回一个新的Stream。
3.7.2.去重
对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct():
List.of("A", "B", "A", "C", "B", "D")
.stream()
.distinct()
.collect(Collectors.toList()); // [A, B, C, D]
3.7.3.截取
截取操作常用于把一个无限的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。
3.7.4.合并
将两个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]
3.7.5.flatMap
如果Stream的元素是集合:
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));
而我们希望把上述Stream转换为Stream< Integer >,就可以使用flatMap():
Stream<Integer> i = s.flatMap(list -> list.stream());
因此,所谓flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream:
3.7.6.并行
通常情况下,对Stream的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。
把一个普通Stream转换为可以并行处理的Stream非常简单,只需要用parallel()进行转换:
Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
.sorted() // 可以进行并行排序
.toArray(String[]::new);
经过parallel()转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。
3.7.7.其他聚合方法
除了reduce()和collect()外,Stream还有一些常用的聚合方法:
- count():用于返回元素个数;
- max(Comparator<? super T> cp):找出最大元素;
- min(Comparator<? super T> cp):找出最小元素。
针对IntStream、LongStream和DoubleStream,还额外提供了以下聚合方法:
- sum():对所有元素求和;
- average():对所有元素求平均数。
3.7.8.其他方法
还有一些方法,用来测试Stream的元素是否满足以下条件:
- boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件; boolean
- anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。
最后一个常用的方法是forEach(),它可以循环处理Stream的每个元素,我们经常传入System.out::println来打印Stream的元素:
Stream<String> s = ...
s.forEach(str -> {
System.out.println("Hello, " + str);
});
3.7.9.小结
Stream提供的常用操作有:
- 转换操作:map(),filter(),sorted(),distinct(),skip(),limit();
- 合并操作:concat(),flatMap();
- 并行处理:parallel();
- 聚合操作:reduce(),collect(),count(),max(),min(),sum(),average();
- 其他操作:allMatch(), anyMatch(), forEach()。