目录
集合是 Java
中使用最多的 API
,几乎每个 Java
程序都需要 制造 和 处理 集合。为了更好地操作集合,JDK
提供了不少工具类,还有不少第三方类库,比如 Guava
、Common Collections
等,但是,还不够。
比如:
在所有职员中,查找年龄小于 40,按工资进行倒序排序,列举工资最高的前十位的职员姓名
在 JAVA8
以前,需要进行专门处理,如果没有注释辅助,很难一眼看明白这段代码的含义
public static void opBefore(List<Emp> emps) {
List<Emp> filterEmps = new ArrayList<>();
for (Emp emp : emps) {
if (emp.getAge() < 40) {
filterEmps.add(emp);
}
}
Collections.sort(filterEmps, new Comparator<Emp>() {
@Override
public int compare(Emp e1, Emp e2) {
return Double.compare(e2.getSalary(), e1.getSalary());
}
});
List<String> names = new ArrayList<>();
for (Emp emp : filterEmps) {
names.add(emp.getName());
if (names.size() >= 10) {
break;
}
}
System.out.println(names);
}
JAVA8
以后呢?JAVA8
的设计者们设计了 流,使用流,只需要如下代码即可表达同样的效果,代码简洁清晰,关键还能一眼看明白含义,看完是不是再也不想写以前的代码了。
public static void opAfter(List<Emp> emps) {
emps.stream().filter(v -> v.getAge() < 40)
.sorted(Comparator.comparing(Emp::getSalary).reversed())
.limit(10)
.map(Emp::getName)
.forEach(System.out::println);
}
如果这个列表非常大,需要使用多线程,只需要将 stream()
修改为 parallelStream()
即可。
流
流是 JDK8
的新成员,具备以下特点:
- 声明式编程:代码简洁、易读
- 可复合:在上例中,
filter
过滤之后马上对过滤的结果执行sorted
排序操作,然后对排序结果进行limit
截断操作,这种管道式编程可以互相组合,从而形成复杂逻辑,非常灵活 - 可并性:只需要简单地方法调用,即可透明地实现并发编程,不用担心内部实现细节,实现高性能
只遍历一次
流跟迭代器一样,只能遍历一次,再次遍历则会抛出异常,如果想再次遍历,只能通过数据源重新获取一个新的流。
List<String> langs = Arrays.asList("Java", "PHP", "Python");
Stream<String> stream = langs.stream();
stream.forEach(System.out::println); // 打印
stream.forEach(System.out::println); // 抛出 java.lang.IllegalStateException 异常
外部迭代与内部迭代
使用 Collection
接口进行迭代(比如 for-each
),称为外部迭代。Streams
库使用内部迭代。
- 使用
for-each
进行外部迭代
List<String> names = new ArrayList<>();
for (Emp emp : emps) {
filterEmps.add(emp.getName());
}
- 使用
Iterator
进行外部迭代
List<String> names = new ArrayList<>();
Iterator<Emp> iterator = emps.iterator();
while(iterator.hasNext()) {
Emp emp = iterator.next();
filterEmps.add(emp.getName());
}
- 使用
Stream
内部迭代
List<String> names = emps.stream()
.map(Emp::getName)
.collect(toList());
外部迭代与内部迭代的区别。外部迭代需要用户显式地取出每个项目再加以处理,一切都靠用户。内部迭代则由 JDK
进行迭代,JDK
可以透明地进行优化处理,比如根据硬件进行优化,使用并行处理等,随着 JDK
的发展,可以继承 JDK
对内部迭代的优化。
流操作
emps.stream()
.filter(v -> v.getAge() < 40)
.sorted(Comparator.comparing(Emp::getSalary).reversed())
.map(Emp::getName)
.limit(10)
.forEach(System.out::println);
中间操作
诸如 filter
、sorted
、map
等,这类操作目标是一个流,操作完成会返回另一个流,这让多个操作可以连接起来,形成一个流水线。中间操作并不会立即处理,而是等待流水线上出现终端操作,在终端操作时一次性全部处理。
方法 | 说明 | 函数描述符 |
---|---|---|
filter | 过滤 | T -> boolean |
map | 对象转换 | T -> R |
peek | 对象处理 | T -> {} |
sorted | 排序 | (T, T) -> Integer |
limit | 截断 | |
distinct | 去重 | |
skip | 跳过 n 个元素 |
终端操作
终端操作会从流的流水线生成结果,这个结果可是以统计,归约,也可以是处理函数,比如:上例中的 forEach
接的就是一个处理函数。
方法 | 说明 |
---|---|
forEach | 处理函数,对流中的每个元素进行处理,比如打印至控制台 |
count | 返回流中元素的个数 |
collect | 归约函数,将流归约成集合,List、Set、Map 等 |
流的使用
构建流
集合构建流
Collection
的实现类(比如:List
、Set
)直接调用 stream()
方法即可获取流。
List<String> langs = Arrays.asList("JAVA", "PHP", "PYTHON")
Stream<String> stream = langs.stream();
数组构建流
数组通过调用 Stream.of()
方法获取流。
String[] langs = {"JAVA", "PHP", "Python"};
Stream<String> stream = Stream.of(langs)
构建无限流
通过 Stream.iterate()
方法构建无限流,这个方法参数是一个表达式,通常会结合 limit
方法进行截断,不然真的无限流就只能等 OOM
了。
构建一个偶数的流
- 第一个参数,定义流的第一个元素
- 第二个参数,根据流中的最后一个元素,得到一个新的元素,并加入到流中
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
构建随机无限流
通过 Stream.generate()
方法构建无限流,使用 iterate
构建无限流时,流中的每个元素有延续性,因为向流中加入新的元素是根据上一个元素进行生成的。而 generate
则是无延续性的,适合生成随机流。
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
查找与匹配
filter
示例:查找流中的偶数并打印到控制台
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
numbers.stream()
.filter(v -> v % 2 == 0)
.forEach(System.out::println);
输出如下
2
4
6
8
findAny & findFirst
通过 findAny
方法查找流中的任意一个元素,这个方法返回的是 Optional
对象
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
numbers.stream()
.filter(v -> v % 2 == 0)
.findAny()
.ifPresent(System.out::println);
输出如下
2
通过 findFirst
方法查找流中的第一个元素,这两个方法的区别在于 findFirst
查找第一个,当流使用并行处理时,需要等待所有结果返回、归约、排序后才能确定是否第一个,而 findAny
则是取任意一个,效率更高。所以如果对顺序没有要求,请使用 findAny
。
anyMatch & allMatch & noneMatch
anyMatch
用于判断流中是否有符合条件的元素
示例:判断流中是否存在偶数
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
System.out.println(numbers.stream().anyMatch(v -> v % 2 == 0));
输出如下
true
allMatch
与 anyMatch
类似,anyMatch
判断流中任意一个匹配成功即可,而 allMatch
则需要流中的所有元素全部匹配。
示例:判断流中是否全部都是偶数
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
System.out.println(numbers.stream().allMatch(v -> v % 2 == 0));
输出如下
false
noneMatch
与 allMatch
正好相反,判断流中的所有元素全部不匹配。
示例:判断流中是否全部都不是偶数
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);
System.out.println(numbers.stream().noneMatch(v -> v % 2 == 0));
输出如下
false
类 SQL 操作
去重
distinct
这个名字看上去是不是有点熟悉,select distinct col ... from table
,不错,就是 SQL
语句中的去重关键字,在流计算中变成了一个方法,作用也是去重。
示例
List<Integer> numbers = Arrays.asList(1,2,2,3,3,4);
numbers.stream()
.distinct()
.forEach(System.out::println);
输出如下
1
2
3
4
截断与跳过
skip
表示跳过 n
个元素,limit
表示截取多少个元素,这两个方法可以组合成内存分页方法
List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
numbers.stream().skip(6).limit(3).forEach(System.out::println);
输出如下
7
8
9
内存分页工具方法
public static <T> List<T> pagination(List<T> rows, int page, int pageSize) {
if (CollectionUtils.isEmpty(rows)) {
return Collections.emptyList();
}
int totalRows = rows.size();
int skipRows = (page - 1) * pageSize;
if (totalRows <= skipRows) {
return Collections.emptyList();
}
return rows.stream().skip(skipRows).limit(pageSize).collect(Collectors.toList());
}
映射
流
创建一个员工类,供后续示例使用
public class Emp {
private Long id;
private String name;
private Integer age;
private Double salary;
private Integer sex;
}
map
map
方法接受一个函数作为参数,对流中每一个元素应用该函数,并返回一个新的元素,更换流中的对象。
示例:打印员工的名称
emps.stream() // 流中的元素为 Emp 对象
.map(Emp::getName) // 流中的元素转换为 String 对象
.forEach(System.out::println);
示例:为所有员工加薪1000
emps.stream() // 流中的元素为 Emp 对象
.map(v -> {
v.setSalary(v.getSalary() + 1000D);
return v;
}) // 流中的元素还是 Emp 对象
.collect(Collectors.toList());
peek
如上例所示,在 map
方法中只是对流中的元素进行处理,并不需要更换对象,peek
方法用于处理这种情况
示例:为所有员工加薪1000
emps.stream()
.peek(v -> v.setSalary(v.getSalary() + 1000D)) // 对流中的元素进行处理,但不更换
.collect(Collectors.toList());
flatMap
流的扁平化,在前面的示例中,流中的元素都是对象,可以通过 map
方法对每一个元素进行处理,但如果流中的元素是一个集合,该如何操作?
例如:指定一个单词列表 ["Hello", "World"]
,返回包含的所有字母并去重
首先需要将单词分解成字母,然后对所有字母的集合进行去重操作即可,通过前文中的示例,可以知道 distinct
方法可以去重,于是组合成如下代码
List<String> words = Arrays.asList("Hello", "World");
List<String[]> letters = words.stream()
.map(v -> v.split(""))
.distinct()
.collect(Collectors.toList());
可以看到,返回的结果是 List<String[]>
类型,并不是想象中的字母集合,这是因为 map
方法返回的是一个字符串数组,map
处理之后,流中的元素变成了字母数组 Stream<String[]>
,而我们实际想要的是 Stream<String>
,通过 flatMap
让流扁平化。
List<String> words = Arrays.asList("Hello", "World");
List<String> letters = words.stream()
.map(v -> v.split("")) // 流中元素转换为 String[]
.flatMap(v -> Stream.of(v)) // 流中元素转换为 String
.distinct()
.collect(Collectors.toList());
自己动手
1、给定一个数字列表,[1,2,3,4],返回每个数字的平方构成的列表,结果为[1,4,9,16]
2、给定两个数字列表,[1,2,3] 及 [3,4],对两个列表进行迪卡尔集,结果为[(1,3),(1,4),(2,3),(2,4),(3,3),(3,4)]
解答1
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
List<Integer> squares = numbers.stream().map(n -> n * n).collect(toList());
解答2
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs = numbers1.stream()
.flatMap(i -> numbers2.stream().map(j -> new int[]{i, j}))
.collect(toList());
归约
在前文中,我们使用了 map
对流中的每个元素进行处理,那么处理完之后,则需要对处理的结果进行收集。在前面的示例中,我们有通过 anyMatch
之类的方法收集为 Boolean
值,或者通过 findAny
之类的方法收集为 Optional
对象,但这些都是定向的收集,流还提供了 reduce
函数用于更复杂的结果归约。
求和
设置初始值
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
无初始值(可能存在流中一个元素都没有的情况)
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
求积
int result = numbers.stream().reduce(1, (a, b) -> a * b);
最大值最小值
求最大值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
求最小值
Optional<Integer> min = numbers.stream().reduce(Integer::min);
统计元素个数
通过 map
将集合中的元素转换为 1
,然后通过 reduce
进行累加即可计算出流中的元素个数。
int count = numbers.stream().map(v -> 1).reduce(0, (a, b) -> a + b);
收集器
流提供了 collect
方法进行结果收集,并提供了 Collectors
类,该类提供了很多收集器,可以基本满足日常需求,如果实现满足不了,也可以通过实现自定义收集器进行收集。
计数器
示例:统计流中员工的数量
long count = emps.Stream().collect(Collectors.counting());
还可以简写为
long count = emps.Stream().count()
比较器
示例:查找年龄最大的员工
Optional<Emp> collect = emps.stream().collect(Collectors.maxBy(Comparator.comparing(Emp::getAge)));
也可以简写为
Optional<Emp> collect = emps.stream().max(Comparator.comparing(Emp::getAge));
示例:查找工资最低的员工
Optional<Emp> collect = emps.stream().collect(Collectors.minBy(Comparator.comparing(Emp::getSalary)));
也可以简写为
Optional<Emp> collect = emps.stream().min(Comparator.comparing(Emp::getSalary));
汇总收集
Collectors
提供了 summingInt
、summingLong
、summingDouble
三个收集器,用于对相应的数据类型进行求和计算。
示例:计算所有员工的薪资总和
Double sumSalary = emps.stream().collect(Comparator.summingDouble(Emp::getSalary));
averagingInt
、averagingLong
、averagingDouble
三个收集器,用于对相应的数据类型进行求平均值。
示例:计算员工的平均薪资
Double avgSalary = emps.stream().collect(Comparator.averagingDouble(Emp::getSalary));
summarizingInt
、summarizingLong
、summarizingDouble
三个收集器,用于对相应的数据类型进行统计,统计结果包括了记录数、汇总值、平均值、最大值、最小值。
DoubleSummaryStatistics collect = emps.stream().collect(Collectors.summarizingDouble(Emp::getSalary));
System.out.println("员工总数:" + collect.getCount());
System.out.println("员工总薪资:" + collect.getSum());
System.out.println("员工平均薪资:" + collect.getAverage());
System.out.println("员工最高薪资:" + collect.getMax());
System.out.println("员工最低薪资:" + collect.getMin());
字符串连接收集器
相当于 String.join()
方法,将字符串集合连接成字符串,可以指定元素之间的间隔字符。
示例:获取所有员工的姓名,并以字符串形式输出
String names = emps.stream().map(Emp::getName).collect(Collectors.joining(","));
List 收集器
示例:获取员工的姓名列表
List<String> names = emps.Stream()
.map(Emp::getName)
.collect(Collectors.toList());
Set 收集器
通过 Set
本身的特性可以实现去重
示例:获取员工的 ID
集合
Set<String> names = emps.Stream()
.map(Emp::getId)
.collect(Collectors.toSet());
Map 收集器
示例:获得 员工ID -> 员工信息
的键值对
Map<Long, Emp> names = emps.Stream()
.map(Emp::getName)
.collect(Collectors.toMap(Emp::getId, Function.identity()))
如果出现 id
一样的记录,以上代码就会出现异常,因为产生了键冲突,又没有提供一个解决冲突的方案,事实上 toMap
方法的第三个参数就是解决冲突的函数,如下例,如果出现键冲突,则抛弃后来的元素,保留第一个。
Map<Long, Emp> names = emps.Stream()
.map(Emp::getName)
.collect(Collectors.toMap(Emp::getId, Function.identity(), (v1, v2) -> v1))
分组收集器
示例:根据性别进行分组
Map<Integer, List<Emp>> sexGroup = emps.stream()
.collect(Collectors.groupingBy(Emp::getSex));
示例:根据年龄进行分组
Map<String, List<Emp>> ageGroup = emps.stream()
.collect(Collectors.groupingBy(v -> {
return v.getAge() <= 35 ? "safety" : "dangerous";
}));
多级分组收集器
groupingBy
收集器还可以接收第二个参数,类型是一个收集器,可以理解为在组内进行二次收集,那么再传入一个分组收集器,则可以实现多级分组收集
示例:先按性别,然后按年龄进行二级分组
Map<Integer, Map<String, List<Emp>>> collect = emps.stream()
.collect(Collectors.groupingBy(Emp::getSex, Collectors.groupingBy(v -> {
return v.getAge() <= 35 ? "safety" : "dangerous";
})));
组内收集
groupingBy
的第二个参数为收集器,可以进行组内收集。
结合 counting
收集器,统计组内记录数
示例:查找不同性别的人数总和
Map<Integer, Long> sexCount = emps.stream()
.collect(Collectors.groupingBy(Emp::getSex, Collectors.counting()));
结合 maxBy
收集器,查找组内最大值
示例:查找不同性别年龄最大的员工
Map<Integer, Optional<Emp>> maxAge = emps.stream()
.collect(Collectors.groupingBy(Emp::getSex, Collectors.maxBy(Comparator.comparing(Emp::getAge))));
组内收集并转换
Collectors
提供了 collectingAndThen
收集器,collectingAndThen
的第一个参数是一个收集器,也是实际执行的收集器,第二个参数为转换器,用于对收集器的收集结果进行转换。
示例:查找不同性别年龄最大的员工
Map<Integer, Emp> maxAge = emps.stream()
.collect(Collectors.groupingBy(Emp::getSex,
Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparing(Emp::getAge)), Optional::get)));
组内转换
Collectors
提供了 mapping
收集器,可以对组内元素进行转换并收集
示例:获取不同性别员工的姓名集合
Map<Integer, List<String>> collect = emps.stream()
.collect(Collectors.groupingBy(Emp::getSex,
Collectors.mapping(Emp::getName, Collectors.toList())));
示例:按性别进行分组,每个组内转换为 id -> name
的键值对
Map<Integer, Map<Long, String>> collect = emps.stream()
.collect(Collectors.groupingBy(Emp::getSex,
Collectors.mapping(Function.identity(), Collectors.toMap(Emp::getId, Emp::getName))));
分区
Collectors
提供了 partitioningBy
分区收集器,这个收集器跟 groupBy
收集器类似,只是收集的 Key
规定为 Boolean
类型
示例:按年龄进行分区,35岁以下的为 true
,否则为 false
,分为两组
Map<Boolean, List<Emp>> collect = emps.stream()
.collect(Collectors.partitioningBy(v -> v.getAge() <= 35));