流的使用
文章目录
筛选和切片(filter, limit, skip)
用谓词筛选
Streams
接口支持filter()
方法。该操作会接受一个谓词(一个返回boolean
的函数)作为参数,并返回一个包括所有符合谓词的元素的流。
例如,筛选出所有素菜,创建一张素食菜单:
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian) // Predicate<T>做参数
.collect(toList());
vegetarianMenu.forEach(System.out::println);
筛选各异的元素
流还支持一个叫作distinct()
的方法,它会返回一个元素各异(根据流所生成元素的hashCode
和equals
方法实现)的流。
例如,以下代码会筛选出列表中所有的偶数,并确保没有重复
// Filtering unique elements
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0) // 筛选出偶数
.distinct() // 去重
.forEach(System.out::println);
截断流-limit
流支持limit(n)
方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit
。
如果流是有序的,则最多会返回前n
个元素。
比如,你可以建立一个List
,选出热量超过300卡路里的头三道菜:
🎉 请注意limit
也可以用在无序流上,比如源是一个Set
。这种情况下,limit
的结果不会以任何顺序排列。
// Truncating a stream
List<Dish> dishesLimit3 = menu.stream()
.filter(d -> d.getCalories() > 300) // 卡路里大于300的食物
.limit(3) // 取得前三个
.collect(toList());
dishesLimit3.forEach(System.out::println);
跳过元素-skip
流还支持skip(n)
方法,返回一个扔掉了前n
个元素的流。
如果流中元素不足n
个,则返回一个空流。
请注意,limit(n)
和skip(n)
是互补的!
例如,下面的代码将跳过超过300卡路里的头两道菜,并返回剩下的
List<Dish> dishesSkip2 = menu.stream()//
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());
dishesSkip2.forEach(System.out::println);
映射(map, flatmap)
一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。
Stream API也通过map
和flatMap
方法提供了类似的工具。
对流中的每一个元素应用函数-map
流支持map
方法,它会接受一个函数作为参数。
这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它是“创建一个新版本”而不是去“修改”)。
例如,下面的代码把方法引用Dish::getName
传给了map
方法,来提取流中菜肴的名称:
// map
// 因为getName方法返回一个String,所以map方法输出的流的类型就是Stream<String>。
List<String> dishNames = Dish.menu.stream()
.map(Dish::getName)
.collect(toList());
System.out.println(dishNames);
// 如果你要找出每道菜的名称有多长
List<String> dishNames = Dish.menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(toList());
System.out.println(dishNames);
流的扁平化-flatMap
对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?
例如,给定单词列表["Hello","World"]
,你想要返回列表["H","e","l", "o","W","r","d"]
。
问题版本
你可能会认为这很容易,你可以把每个单词映射成一张字符表,然后调用distinct
来过滤重复的字符。第一个版本可能是这样的:
public static void main(String[] args) {
List<String> words = new ArrayList<>();
words.add("Hello");
words.add("world");
List<String[]> collect = words.stream()
.map(word -> word.split("")) // Stream<String[]>
.distinct() // Stream<String[]>
.collect(Collectors.toList());
}
这个方法的问题在于,传递给map方法的Lambda为每个单词返回了一个String[](String列 表 )。
因此 , map返回的流实际上是 Stream<String[]>
类型的 。 而你真正想要的是用Stream<String>
来表示一个字符串流。
解决之道
尝试使用map和Array.stream()
String[] arrayOfWords = {"Hello", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
//返回的并不是想要的List<String>
List<Stream<String>> list = words.stream()
.map(word -> word.split(""))// 返回Stream<String[]>
.map(Arrays::stream) // 返回Stream<Stream<String>> -------- flatMap
.distinct()
.collect(toList());
2.使用flatMap
List<String> uniqueCharacters =
words.stream()
.map(w -> w.split(""))// 返回Stream<String[]>
.flatMap(Arrays::stream)// 返回Stream<String>,把Stream<Stream<String>> 压成 Stream<String>
.distinct()
.collect(Collectors.toList());
一言以蔽之,flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
PS. flatmap 能把
Stream<Stream<String>>
压成Stream<String>
//简化了一些
words.stream()
.flatMap((String line) -> Arrays.stream(line.split("")))
.distinct()
.forEach(System.out::println);
查找和匹配(find, match)
检查谓词是否至少匹配一个元素-anyMatch
private static boolean isVegetarianFriendlyMenu() {
return Dish.menu.stream().anyMatch(Dish::isVegetarian);
}
检查谓词是否匹配所有元素-allMatch
private static boolean isHealthyMenu() {
return Dish.menu.stream().allMatch(d -> d.getCalories() < 1000);
}
检查谓词是否不匹配所有元素-noneMatch
private static boolean isHealthyMenu2() {
return Dish.menu.stream().noneMatch(d -> d.getCalories() >= 1000);
}
anyMatch、allMatch和noneMatch这三个操作都用到了所谓的短路,这就是大家熟悉的Java中&&和||运算符短路在流中的版本
查找元素-findAny
流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束
Optional<Dish> dish =menu.stream()
.filter(Dish::isVegetarian)
.findAny();
等等,上面的Optional是个啥?
Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。
在上面的代码中,findAny可能什么元素都没找到。Java 8的库设计人员引入了Optional<T>,这样就不用返回众所周知容易出问题的null了。
Optional里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法也不错,使用熟练之后可以有效的避免空指针问题。
- isPresent()将在Optional包含值的时候返回true, 否则返回false。
- ifPresent(Consumer<T> block)会在值存在的时候执行给定的代码块。
- T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。
- T orElse(T other)会在值存在时返回值,否则返回一个默认值。
查找第一个元素-findFirst
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 因为可能一个都找不到,所以返回的是Optional
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
.map(x -> x * x) // 对于每一个x都变成x^2
.filter(x -> x % 3 == 0) // 将是3的倍数的过滤出来
.findFirst(); // 找到的第一个就是
何时使用findFirst和findAny
为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素在并行上限制更多。
如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
归约(reduce)
到目前为止,见到过的终端操作都是返回一个boolean(allMatch
之类的)、void(forEach
)或Optional对象(findAny
等)。
你也见过了使用collect
来将流中的所有元素组合成一个List
。
在本节中,你将看到如何把一个流中的元素组合起来,使用reduce
操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是哪一个”。
此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer
。
这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold)
元素求和
在研究如何使用reduce
方法之前,先来看看如何使用for-each
循环来对数字列表中的元素求和:
List<Integer> numbers = Arrays.asList(4, 5, 3, 9);
int sum = 0;
for (Integer number : numbers) {
sum += number;
}
System.out.println(sum);
numbers
中的每个元素都用加法运算符反复迭代来得到结果。通过反复使用加法,你把一个数字列表归约成了一个数字。这段代码中有两个参数:
- 总和变量的初始值,在这里是
0
; - 将列表中所有元素结合在一起的操作,在这里是
+
。
要是还能把所有的数字相乘,而不必去复制粘贴这段代码,岂不是很好?这正是reduce
操作的用武之地,它对这种重复应用的模式做了抽象。
你可以像下面这样对流中所有的元素求和:
// 结果被包裹在一个Optional对象里,以表明和可能不存在
Optional<Integer> reduce = numbers.stream().reduce(Integer::sum);
System.out.println(reduce.get());
元素求积
你也很容易把所有的元素相乘,只需要将另一个Lambda:(a, b) -> a * b
传递给reduce
操作就可以了:
Optional<Integer> reduce1 = numbers.stream().reduce((a, b) -> a * b);
System.out.println(reduce1.get());
最大值和最小值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
个数统计
// 方法一:d -> 1
int count = numbers.stream()
.map(d -> 1)
.reduce(0, Integer::sum);
System.out.println(count);
// 方法二:count()
long count2 = numbers.stream().count();
System.out.println(count2);
归约方法的优势与并行化
相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。
而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。
如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!
这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了。
使用流来对所有的元素并行求和时,代码几乎不用修改:stream()
换成了parallelStream()
。
int sum = numbers.parallelStream().reduce(0, Integer::sum);
流操作:无状态和有状态
诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态。
但诸如reduce、sum、max等操作需要内部状态来累积结果。
在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。
相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。
从流中排序和删除重复项时都需要知道先前的历史。这一操作的存储要求是无界的。
要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。
中间操作和终端操作小结
操作 | 类型 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 |
---|---|---|---|---|
filter | 中间 | Stream<T> | Predicate<T> | T->boolean |
distinct | 中间 (有状态-无界) | Stream<T> | - | - |
skip | 中间 (有状态-有界) | Stream<T> | long | - |
limit | 中间 (有状态-有界) | Stream<T> | long | - |
map | 中间 | Stream<R> | Function<T,R> | T->R |
flatMap | 中间 | Stream<R> | Function<T,Stream<R>> | T->Stream<R> |
sorted | 中间 (有状态-无界) | Stream<T> | Comparator<T> | (T,T)->int |
anyMatch | 终端 | boolean | Predicate<T> | T->boolean |
noneMatch | 终端 | boolean | Predicate<T> | T->boolean |
allMatch | 终端 | boolean | Predicate<T> | T->boolean |
findAny | 终端 | Optional<T> | - | - |
findFirst | 终端 | Optional<T> | - | - |
forEach | 终端 | void | Consumer<T> | T->void |
collect | 终端 | R | Collector<T,A,R> | - |
reduce | 终端 (有状态-有界) | Optional<T> | BinaryOperator<T> | (T,T)->T |
count | 终端 | long | - | - |
数值流
原始类型流特化
Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。
每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。
要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。
映射到数值流-mapToXXX
将流转换为特化版本的常用方法是mapToInt、 mapToDouble和mapToLong。
int calories = menu.stream()
.mapToInt(Dish::getCalories) //返回一个IntStream,不是Stream<Integer>
.sum();
请注意,如果流是空的,sum默认返回0。IntStream还支持其他的方便方法,如max、min、average等。
对于List -> int[]/long[]/double[], 可以使用对应的map操作
int[] numbers = list.stream().mapToInt(Integer::valueOf).toArray();
转换回对象流-boxed
同样,一旦有了数值流,你可能会想把它转换回非特化流。
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
所以对于int[]/long[]/double[] -> List,可以使用对应的装箱操作
// int[] -> list
int[] numbers = new int[]{1, 2, 3, 4, 5, 6};
// java 8+才可以使用
// 流化Arrays.stream(numbers) IntStram
// -> 装箱boxed【IntStream -> Stream<Integer>】
// -> 转成list:collect(Collectors.toList())
List<Integer> list = Arrays.stream(numbers).boxed().collect(Collectors.toList());
默认值-OptionalInt
如果你要计算IntStream中的最大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢?
Optional可以用Integer、String等参考类型来参数化。
对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt
、OptionalDouble
和OptionalLong
。
例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt:
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
现在,如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了:
int max = maxCalories.orElse(1);
数值范围:XXXStream.range()/rangeClosed()
Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。
这两个方法都是第一个参数接受起始值,第二个参数接受结束值。
但range是不包含结束值的【左闭右开】,而rangeClosed则包含结束值【左闭右闭】。
// 下面两种表示都会生成[1, 100]的整数
IntStream range = IntStream.range(1, 101);
range.forEach(System.out::println);
IntStream range2 = IntStream.rangeClosed(1, 100);
range2.forEach(System.out::println);
流的构建
由值创建流:Stream.of()
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
//你可以使用empty得到一个空流,如下所示:
Stream<String> emptyStream = Stream.empty();
由数组创建流:Arrays.stream()
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
由文件生成流:Files.line()
long uniqueWords = 0;
//流会自动关闭
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
}
catch(IOException e){
}
由函数生成流(无限流):
Stream API提供了两个静态方法来从函数生成流:Stream.iterate()
和Stream.generate()
。
这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。
由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!
一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
迭代-iterate
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator<T>类型)。
迭代-斐波纳契数列
//序列(0, 1), (1, 1), (1, 2), (2, 3), (3, 5), (5, 8), (8, 13), (13, 21)...
Stream.iterate(new int[]{0, 1},t -> new int[]{t[1], t[0]+t[1]})
.limit(20)
.forEach(t -> System.out.println("(" + t[0] + "," + t[1] +")"));
//只想打印正常的斐波纳契数列
Stream.iterate(new int[]{0, 1},t -> new int[]{t[1],t[0] + t[1]})
.limit(10)
.map(t -> t[0])
.forEach(System.out::println);
生成-generate
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
我们使用的供应源(指向Math.random的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。
可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。
举个例子,接下来将展示如何利用generate创建斐波纳契数列,这样你就可以和用iterate方法的办法比较一下。
但很重要的一点是,在并行代码中使用有状态的供应源是不安全的。因此下面的代码仅仅是为了内容完整,应尽量避免使用
IntStream.generate(() -> 1)
.limit(5)
.forEach(System.out::println);
IntStream twos = IntStream.generate(new IntSupplier(){
public int getAsInt(){
return 2;
}
});
生成-斐波纳契数列
IntSupplier fib = new IntSupplier(){
private int previous = 0;
private int current = 1;
public int getAsInt(){
int oldPrevious = this.previous;
int nextValue = this.previous + this.current;
this.previous = this.current;
this.current = nextValue;
return oldPrevious;
}
};
IntStream.generate(fib).limit(10).forEach(System.out::println);
前面的代码创建了一个IntSupplier的实例。此对象有可变的状态:它在两个实例变量中记录了前一个斐波纳契项和当前的斐波纳契项。
getAsInt在调用时会改变对象的状态,由此在每次调用时产生新的值。
相比之下,使用iterate的方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元组。
请注意,因为你处理的是一个无限流,所以必须使用limit操作来显式限制它的大小;否则,终端操作(这里是forEach)将永远计算下去。
同样,你不能对无限流做排序或归约,因为所有元素都需要处理,而这永远也完不成!