将一个stream映射成另一个stream
使用map()方法将一个流映射到另一个流,该方法将此函数作为参数。映射流意味着该流处理的所有元素都将使用该函数进行转换。
List<String> strings = List.of("one", "two", "three", "four");
Function<String, Integer> toLength = String::length;
Stream<Integer> ints = strings.stream()
.map(toLength);
运行上面这段代码并不会输出任何结果,因为还没有调用整合数据操作。
正确的代码如下:
List<String> strings = List.of("one", "two", "three", "four");
List<Integer> lengths = strings.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println("lengths = " + lengths);
输出:
lengths = [3, 3, 5, 4]
可以看到这种方式通过map(String::length)返回一个Stream。同时也可以通过调用mapToInt()替换map()
来返回一个IntStream 。
但是没有Collector的collect()方法提供给特殊的stream,特殊的stream只能获取到统计信息。
List<String> strings = List.of("one", "two", "three", "four");
IntSummaryStatistics stats = strings.stream()
.mapToInt(String::length)
.summaryStatistics();
System.out.println("stats = " + stats);
输出结果如下:
stats = IntSummaryStatistics{count=4, sum=15, min=3, average=3,750000, max=5}
过滤stream
过滤意味着使用predicat对流处理的元素进行筛选和丢弃。
下面这段代码是统计长度为3的字符的数量:
List<String> strings = List.of("one", "two", "three", "four");
long count = strings.stream()
.map(String::length)
.filter(length -> length == 3)
.count();
System.out.println("count = " + count);
输出结果如下:
count = 2
使用stream处理1:p的关系
操作示例:flatmap
假设你有两个实体,state和city,一个state包含了多个city。
public class City {
private String name;
private int population;
// constructors, getters
// toString, equals and hashCode
}
public class State {
private String name;
private List<City> cities;
// constructors, getters
// toString, equals and hashCode
}
假设你现在处理一个list的state,统计所有city的population,
List<State> states = ...;
int totalPopulation = 0;
for (State state: states) {
for (City city: state.getCities()) {
totalPopulation += city.getPopulation();
}
}
System.out.println("Total population = " + totalPopulation);
内循环的代码可以写成如下:
totalPopulation += state.getCities().stream().mapToInt(City::getPopulation).sum();
这段代码看起来并不优雅,并且stream和state匹配并不好。
接下来是flatmap的处理时刻。
flatmap在对象和stream之间提供了一对多的关系。flatmap使用一个特殊的function作为参数,返回一个stream。
在上面的例子中有一个list的city在state中,所以function很简单:
Function<State, Stream<City>> stateToCity = state -> state.getCities().stream();
flatmap使用两步来处理一个stream:
第一步是用function映射stream中的所有元素,对于每个stream,映射一个Stream<Stream>。
第二步是将Stream<Stream>转化为Stream。
所以代码如下:
List<State> states = ...;
int totalPopulation =
states.stream()
.flatMap(state -> state.getCities().stream())
.mapToInt(City::getPopulation)
.sum();
System.out.println("Total population = " + totalPopulation);
使用Flatmap 和 MapMulti检查元素的传输
来看下面的例子:
假设你有一个list的string,要把它们转换成Integer,其中有错误的数字,如空串,null,空格等。需要把这些错误的去除而把正确的数字留下。你可能会这么做:
Predicate<String> isANumber = s -> {
try {
int i = Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
};
第一步是直接将字符串转换为数字,看是否能正常转换,第二步在map的function中再做一次转换。千万不要这样做,从try catch中返回不是一个好的做法。
真实需要做的是,对于每一个string,如果能转换成数字,返回一个stream,如果不能,返回一个空的stream。
这正是flatmapper所作的事。
Function<String, Stream<Integer>> flatParser = s -> {
try {
return Stream.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
}
return Stream.empty();
};
List<String> strings = List.of("1", " ", "2", "3 ", "", "3");
List<Integer> ints =
strings.stream()
.flatMap(flatParser)
.collect(Collectors.toList());
System.out.println("ints = " + ints);
结果如下:
ints = [1, 2, 3]
这种方式解决了问题,但并不是最好的方法。因为function在处理元素时,为每个元素创建了一个stream,有过载的问题。从java se16开始,stream api增加了一个方法,mapMulti(),用来处理这种情况:遇到要创建很多空的或只有一个元素的stream。
这个方法使用的是BiConsumer 作为参数,BiConsumer 有两个参数:
stream中需要映射的元素
一个Consumer:BiConsumer 中需要与映射结果一起调用
使用每个元素调用consumer会添加元素到结果stream中,一旦映射不能完成,biconsumer 不会调用consumer,元素将不会被加到结果stream中。
List<Integer> ints =
strings.stream()
.<Integer>mapMulti((string, consumer) -> {
try {
consumer.accept(Integer.parseInt(string));
} catch (NumberFormatException ignored) {
}
})
.collect(Collectors.toList());
System.out.println("ints = " + ints);
输出:
ints = [1, 2, 3]
去除重复和排序
stream有去重和排序方法distinct() 和 sorted(),distinct() 方法使用hashCode() 和 equals()方法进行去重。sorted()方法使用了重载,如果提供了comparator,那么使用comparator排序,如果没有提供,stream会假设处理的数据已经实现comparable接口,否则会抛ClassCastException 异常。
在之前有提到,stream是不存储数据的,除了一些例外。而distinct() 和 sorted()就是属于这种例外。
限制和跳过流的元素
stream Api提供了两种方法来从流中选择元素:
第一种方法是skip() 和 limit(),类似于sql查询:
List<Integer> ints = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> result =
ints.stream()
.skip(2)
.limit(5)
.collect(Collectors.toList());
System.out.println("result = " + result);
输出:
result = [3, 4, 5, 6, 7]
Java SE 9在这个领域引入了另外两种方法。它不是基于流中的索引跳过和限制元素,而是基于Predicate的值。
- dropWhile(Predicate)删除流处理的元素,直到这些元素上的Predicate为真。此时,该流处理的所有元素都被传输到下一个流。
- takeWhile(predicate)则相反:它将元素传输到下一个流,直到这个Predicate在这些元素上的应用变成false。
这两个方法的工作原理类似于门。一旦dropWhile()打开了让已处理元素流动的门,它就不会关闭它。一旦takeWhile()关闭了门,它就不能重新打开它,就不会再有元素被发送到下一个操作。
流的连接
Stream Api 提供了几种方式来处理流的连接,最常见的是 stream 接口中的concat().
该方法接受两个流并生成一个流,其中包含第一个流生成的元素,然后是第二个流的元素。
您可能想知道为什么这个方法不采用可变参数来允许任意数量的流的连接。
原因是当你有两个流,使用concat()是可以的。如果超过两个,javaDoc api 文档建议使用 flatmap()
List<Integer> list0 = List.of(1, 2, 3);
List<Integer> list1 = List.of(4, 5, 6);
List<Integer> list2 = List.of(7, 8, 9);
// 1st pattern: concat
List<Integer> concat =
Stream.concat(list0.stream(), list1.stream())
.collect(Collectors.toList());
// 2nd pattern: flatMap
List<Integer> flatMap =
Stream.of(list0.stream(), list1.stream(), list2.stream())
.flatMap(Function.identity())
.collect(Collectors.toList());
System.out.println("concat = " + concat);
System.out.println("flatMap = " + flatMap);
输出:
concat = [1, 2, 3, 4, 5, 6]
flatMap = [1, 2, 3, 4, 5, 6, 7, 8, 9]
使用 flatMap()更好的真正原因是concat()会创建中间流在连接流时。当你使用concat()时,每两个流的连接就会产生一个中间流。所以每次连接都会有一个新的流被创建然后很快被丢弃。
使用flatmap模式,只需要创建一个流去处理所有的流。
看起来concat()一点用都没有。实际上在concat()和flatmap()之间有一点区别。
如果所要连接的两个流的元素的数量是确定的,那么产生的新流的大小也是确定的。
使用flatmap产生的新流的大小可能是不确定的,stream api 会失去所处理的元素的数量的追踪。
换句话来说,concat()产生固定大小的流,而flatmap不会。
debug stream
有时在运行时检查处理的元素很方便。
stream的peek() 方法是用来调试的。但不能使用这个方法在生产的环境中。
应该避免在你的应用程序中使用这个方法执行产生的副作用。
List<String> strings = List.of("one", "two", "three", "four");
List<String> result =
strings.stream()
.peek(s -> System.out.println("Starting with = " + s))
.filter(s -> s.startsWith("t"))
.peek(s -> System.out.println("Filtered = " + s))
.map(String::toUpperCase)
.peek(s -> System.out.println("Mapped = " + s))
.collect(Collectors.toList());
System.out.println("result = " + result);
输出:
Starting with = one
Starting with = two
Filtered = two
Mapped = TWO
Starting with = three
Filtered = three
Mapped = THREE
Starting with = four
result = [TWO, THREE]
peek()方法会一个接一个的遍历stream中的元素。