迭代与流
与集合相比,流提供了一种可以让我们在更高的概念级别上指定任务的数据视图。
流提供了几种操作函数,其调用要比一般的迭代更加简洁。并且其可以链式调用,每一次调用都返回一个新的流对象。
如:统计一个List中长度大于12的单词数量。
迭代
1 | int count = 0; |
流
1 | long count = words.stream().filter(w -> w.length() > 12).count(); |
流与集合的差异
- 流并不存储其元素。这些元素可能存储在底层的集合中,或者是按需生成的。
- 流的操作并不会修改其数据源。如
filter
方法不会从流中移除元素,二十会生成一个新的流,其中不包含过滤掉的元素。 - 流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。例如,如果只是想查找前5个单词而不是所有的单词,那么
filter
方法会在匹配到第5个单词后就停止过滤。因此,我们甚至可以操作无限流。
流的创建
从集合Collections
接口创建流
对于实现了Collection
接口的类。可以使用Stream.of
方法来获得一个接口。详见java核心技术-I-9-集合
如:
1 | ArrayList<String> arrayList = new ArrayList<>(); |
从数组生成Stream
用所有元素生成Stream
如果要用所有的数组元素来生成一个Stream
,则可以使用Stream.of
如:
1 | String[] arr = {"apple", "banana", "pear"}; |
用部分数组元素(连续)生成Stream
如果只需要数组的一部分元素(连续)生成Stream
,则可以使用Array.stream(array, from, to)
。
如:
1 | String[] arr = {"apple", "banana", "pear"}; |
直接从基本元素生成Stream
可以通过Stream.of
来将基本元素转变为Stream
。
如:
1 | Stream stream2 = Stream.of("apple", "banana"); |
创建一个空流
可以通过Stream.empty()
来生成一个不包含任何元素的流。
如:
1 | Stream<String> silence = Stream.empty(); |
创建一个无限流
可以通过Stream.generate(FunctionInterface)
来获取一个无线流,其来源是参数中的函数式接口。
如-通过一个字符串创建流:
1 | Stream<String> echos = Stream.generate(() -> "Echo"); |
如-通过随机函数来获取一个包含无限随机数的流:
1 | Stream<Double> echos = Stream.generate(Math::random); |
创建一个无线的序列流
可以通过Stream.iterate(Iterable)
来通过迭代器创建一个指定的序列流。
如:
1 | Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)); |
创建一个长度仅为0或1的流
通过Stream.ofNullable(T t)
方法可以创建一个长度仅为0或1的流。当参数t为null时,则长度为0,否则长度为1,即是包含该对象。
注意
值得注意的是,在执行流的操作的时候,我们并没有修改流背后的集合。但是如果手动修改了流的集合(指在流操作外部修改,在流操作,比如forEach
中,则会报错),那么流的操作就会变成不可预知的。JDK文档称之为不干涉性。
操作
filter(Predicate<T>)
filter
函数会按照参数中的函数式接口来过滤所有元素。然后将符合条件的元素组合为新的流返回。
如-过滤所有长度大于12的单词:
1 | List<String> words = ...; |
注意filter
的参数为一个Predicate<T>
函数式接口。即从T到Boolean的函数。
map(FunctionInterface)
map(FunctionInterface)
会对流中的每个元素应用参数的函数式接口。并且将所有的结果组合为一个新的流返回。
如-截取所有单词的首字母:
1 | List<String> words = ...; |
flatMap(FunctionInterface)
flatMap(FunctionInterface)
是对流中的每个元素应用参数的函数式接口(其都返回一个流),然后将每个参数的返回值组合为一个流。
如codePoints("boat")
返回的流是["b", "o", "a", "t"]
所以Stream<Stream<String>> result = words.stream().map(w -> codePoints(w))
会返回一个流的嵌套。[...["a", "b"], ["c", "d"]]
而使用Stream<Stream<String>> result = words.stream().flatMap(w -> codePoints(w))
会返回一个所有子流的组合流,即[..., "a", "b", "c", "d"]
stream.limit(n)
stream.limit(n)
会截取流中的前n个元素(如果其长度小于n,则会返回流中的全部元素组成的新流)。
这个方法可以用于裁剪无限流。
如-获取100个随机数的流
1 | Stream<Double> randoms = Streams.generate(Math::random).limit(100); |
stream.skip(n)
stream.skip(n)
方法正好相反,其会丢弃前n个元素。
如-跳过单词表中的前5个元素
1 | Stream<String> newWords = words.stream().skip(5); |
stream.takeWhile(predicate)
stream.takeWhile(predicate)
会在谓词为真时获取该元素。
如-收集分割字符串中的所有数字元素
1 | Stream<String> initialDigits = codePoints(str).takeWhile(s -> "0123456789".contains(s)); |
stream.takeWhile(predicate)
takeWhile
方法正好相反,他会在条件为真的时候丢弃元素。
如-收集分割字符串中的所有非数字元素
1 | Stream<String> initialDigits = codePoints(str).dropWhile(s -> "0123456789".contains(s)); |
Stream.concat(Stream stream1, Stream stream1)
与字符串一样,conact
方法可以将两个流的元素连接在一起。
1 | Stream<String> s = Stream.concat(codePoints("hello"), codeoPints("world")); |
stream.disinct()
stream.disinct()
会将原流中的重复元素剔除之后,返回一个新的无重复元素的流。
如:剔除重复字母
1 | Stream<String> s = Stream.of("a", "b", "c", "b", "a").disinct(); |
stream.sorted(comparator)
与其他集合排序一样,流的排序也是接受一个comparator
。其会根据这个comparator
,返回排序后的元素。
如-将最长的字符串排在前面
1 | Stream<String> longsFirst = words.stream().sorted(Comparator.comparing(String::length).reverse()); |
stream.peek(FunctionInterface)
stream.peek(FunctionInterface)
的即预览这个流的元素,但是并不会将其取出。(其本质是生成一个新的流,然后获取新流中的元素,所以原来的流并无变化)然后对这个元素应用参数的函数式接口。
如-打印流中的元素
1 | Stream<String> s = Stream.of("a", "b", "c", "b", "a").peek(e -> System.out.println("item-" + e)); |
简单约简(注意以前方法均返回Option
对象,将在下一章介绍)
通过约简可以获得通过前面处理的流的指定结果。约简是一种终结操作,他们会将流约简为可以在程序中使用的非流值。
其有以下API:
stream.max(comparator)
通过比较器返回流中最大的元素。
stream.min(comparator)
通过比较器返回流中最小的元素。
stream.findFirst()
返回流中的第一个元素。如果流为空,则返回空的Option
对象。
stream.findAny()
返回流中的任意一个元素。如果流为空,则返回空的Option
对象。
stream.anyMatch(predicate)
流中的是否存在一个元素满足给定谓词。
stream.allMatch(predicate)
流中是否所有元素是否都满足给定谓词。
stream.noneMatch(predicate)
流中是否没有元素满足给定谓词。
Optional
类型
Optional<T>
对象是一种包装器对象,其可能包装了T对象或者没有包装任意对象。其被视为更安全的方式,用来替代类型T的引用。
获取Optional
值
其有3个方法来获取Opitons
的值。
Opions.orElse(T t)
该方法会在options存在值的时候返回该值,否则返回传递的t。
Options.orElseGet(FunctionInterface)
该方法会在options存在值的时候返回该值,否则返回传递的函数参数调用的结果。
Options.orElseThrow(exceptionSupplier)
该方法会在options存在值的时候返回该值,否则抛出传递参数的异常。
消费Optional
值
除了直接获取optional包装的值,还可以接受一个函数接口,让函数接口直接处理该函数。
optional.ifPresent(Consumer action)
该方法会在值存在的情况下,将其传递给参数函数。否则不会有任何效果。
optional.ifPresentOrElse(Consumer action, Runnable emptyAction)
该方法会在值存在的情况下,将其传递给第一个参数函数;否则会调用第二个空的处理方法。
管道化Optional
值
管道化即获取optional的值,但不消费。可以使用与管道相似的方法来操作Optional
对象,同时返回一个新的0ptional
对象。此时可以将Optional
对象看为一个长度为0或1的流。
其有以下3个API:
optional.map(FunctionInterface)
通过参数函数处理原optional
对象的值。然后返回新的Oprtional
对象。
optional.filter(predicate)
若原Optional
的值满足给定谓词,就返回一个新的一样值的Optional
对象,否则,返回一个空的Optional
对象。
Optional.or(supplier)
如果当前Optional
对象不为空,则产生当前的Optional
,否则由supplier
产生一个Optional
。
用flatMap
构建Optional
值的函数
flatMap
会在Optional
包装部位空时,产生将mapper
应用于当前Optional
值所产生的结果,或者在当前Optional
为空时,返回一个空Optional
。
由于Opional<T>
是一个包装类型,所以无法直接调用T的相关方法。而可以使用flatMap
来直接获取到包装类内部的值。并且调用其方法,然后返回。
如:
类型S有一个f方法来产生一个Optional<T>
对象。
类型T有一个g方法来产生一个Optional<U>
对象。
为我们无法使用s.f().g()
来获得Optional<U>
对象。
但是我们可以使用下面的方式来获取该对象:
1 | Optional<U> res = s.f().flatMap(T::g); |
将Optional
转换为流
通过optional.stream()
可以将Optional
值转变为一个长度为0或1的流。
TIPS
Optional
类型的便利永远都不应该为null;- 不要使用
Optional
类型的域。因为其代价是额外多出来一个对象。在累的内部,使用null表示缺失的域更易于操作。 - 不要在集合中放置
Optional
对象,并且不要将他们作为map的key。应该直接收集其中的值。
收集结果
当处理完流之后,通常需要查看其结果。则可以通过以下方法来查看内部元素。
stream.iterator
可以使用stream.iterator
来生成一个迭代器,则可以按照普通的迭代器方法来查看内部元素。
stream.forEach(Comsumer action)
可以通过stream.forEach
来直接遍历流中的元素。
stream.forEachOrdered(Comsumer action)
使用该方法遍历时,此时会完全按照流中的顺序。但会丧失并行处理的部分甚至全部优势。
stream.toArray(Constructor)
调用此方法会返回一个数组,如果不传递参数,则返回Object[]
,需要强转。其参数为对应类型的构造函数,此时可以返回对应类型的数组。
如:
1 | Stream<Integer> s = Stream.of(1, 2, 3, 4); |
stream.toList()
此方法会返回一个List
用于查看内容。
如:
1 | Stream<Integer> s = Stream.of(1, 2, 3, 4); |
stream.collect(Collector collector)
可以通过该方法将流中的元素收集到指定的集合中。其中Collector collector
一般是通过Collectors.toXXX()
或者Collectors.toCollection(XXX:new)
来获取。
如:
1-收集到List(与上面的API作用相同)
1 | List<String> res = stream.collect(Collections.toList()); |
2-收集到Set
1 | Set<String> res = stream.collect(Collectors.toSet()); |
3-收集到指定的集合
1 | TreeSet<String> res = stream.collect(Collectors.toCollection(TreeSet::new)); |
收集到字符串
还可以通过stream.collect
收集到一个字符串。关键是collector
必须是一个Collectors.join()
产生的。
例如:
1 | String res = stream.collect(Collectors.join()); //不加间隔符 |
如果流中除字符串外还包含其他对象,那么我们需要先将其转化为字符串。
如:
1 | String res = stream.map(Object::toString).collect(Collectors.join(", ")); |
收集到映射表
假设流中的对象是一个Person
对象,而我们要将其收集到映射表中,key
为Person.id
;value
为Person.name
。
则可以使用Collectors.toMap()
方法。该方法接受两个参数,用来产生映射表的键和值。
如:
1 | Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName)); |
如果需要获取对象本身,则使用Funciton.identity()
。即返回的是流中的类型元素T。
如:
1 | Map<Integer, Person> idToPerson = people.collect(Collectors,toMap(Person::getId, Function.identity)); |
嵌套集合
假如要收集每个国家支持的语言。则我们需要一个Map<String, Set<String>>
,此时我们由该如何直接从流中获取呢。
此时的Collectors.map
需要3个参数:
- key值。
- 指定value的值。
- 组合set的函数
例如:
1 | Map<String, Set<String>> countryLang = locales.collect( |
如果需要使用TreeMap
,则需要将其构造器TreeMap::new
指定为第4个参数。
组群和分组
上面嵌套集合的方式可以使用组群和分区更简洁的解决。
Collectors.groupingBy()
如:
1 | Map<String, List<Locale>> countryToLocales = locales.collect( |
其中函数Locale::getCountry
是组群的分类函数。
Collectors.partitioningBy()
当分类函数是一个断言函数(即返回Boolean值的函数),流的元素可能分为两个列表。该函数返回true的元素和其他元素。
如:将locale
分成了使用英语和使用其他所有语言的两类:
1 | Map<Boolean, List<Locale>> englishAndOther = locales.collect( |
下游收集器
groupingBy
方法会产生一个映射表,其每个值都是一个列表。如果想要以某种方式来处理这些列表,则需要下游收集器。
Collectors.counting()
Collectors.counting()
会产生收集到的元素的个数。
如:
1 | Map<String, lOng> countryToLocaleCounts = locales.collect(groupBy(Locale::getCountry, Collectors.counting())); |
可以对每个国家有多少个local进行计数。
Collectors.summing(Int|Long|Double)()
上面的方法会返回收集到的数据的和。
如:
1 | Map<String, Integer> stateToPopulation = cities.collect( |
可以计算城市流中每个州的人口总和。
Collectors.maxBy
&&Collectors.minBy
以上两个方法会接受一个比较器,并分别产生下游元素中的最大值和最小值。
如:
1 | Map<String, Optional<City>> stateToLargestCity = cities.collect( |
Collectors.collectAndThen
该收集器会在收集器后面添加一个最终处理步骤。例如,我们想要知道有多少种不同的结果,那么就可以将他们收集到一个集合中,然后计算其尺寸:
1 | Map<Character, Integer> stringCountByStartingLetter = strings.collect( |
Collectors.mapping
Collectors.mapping
与Collectors.collectAndThen
相反,他会将参数函数应用于收集到的每个元素,并将其结果传递给下游收集器。
如:按照首字母进行收集,咋子每个分组内部,计算字符串的长度并将其传递给下游收集器:
1 | Map<Character, Set<Integer>> stringLengthByStartingLetter = strings.collect( |
Collectors.flatMap
此处作用也是一致,可以将每个组结果返回的结果进行铺平。
Collectors.filtering
Collectors.filtering
会将过滤器应用到每个组上的元素中,如:
1 | Map<String, Set<City>> largeCitiesByState = cities.collect( |
TIPS
将收集器组合起来是一种很强大的方式,但是它也可能会导致产生非常复杂的表达式。最佳用法是与groupingBy
和partitioningBy
一起处理下游的映射表中的值。否则,应该直接在流上应用诸如map
、reduce
、count
、max
和min
这样的方式。
约简操作
reduce
方法是一种用于从流中计算某个值的通用机制。其有3个重载。
Optional<T> reduce(BinaryOperator<T> accumulator);
其接受一个二元表达式,其第一个操作符为上一次调用该表达式的返回值(首次为第一个元素)。
如:对一个流求和:
1 | List<Integer> values = ...; |
T reduce(T identity, BinaryOperator<T> accumulator);
与上面用法相同,只是首次的元素会被替换为identity
。
1 | List<Integer> values = ...; |
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
这种形式用于当我们需要统计流中元素的某个属性进行约简操作时。其中BiFunction
用于累加操作,而accumulator
用于合并并行处理的结果。
如:
1 | int res = words.reduce(0, |
基本类型流
与基本类型的包装类相似,流对于基本类型也有对应的基本类型流:
IntStream
:Integer, Short, Char, Byte, BooleanLongStream
:LongDoubleStream
:Double, Float
与对象流一样,我们还可以使用静态的generate
和iterate
方法。
此外,IntStream
和LongStream
有静态方法range
和rangeClosed
,可以生成步长为1的整数范围(后者包含上限)。
如:
1 | IntStream zeroToNinetyNine = IntStream.range(0, 100); //0-99 |
基本流与对象流的转化
对象流转基本流
可以使用stream
的mapToInt
,mapToLong
,mapToDouble
来将其转换为基本流,可以将原流中的属性转化为新的基本流。
如:将单词的长度转化为新的IntStream
1 | Stream<String> words = ...; |
基本流转对象流
可以使用boxed
方法转化为对象流。
如:
1 | Stream<Integer> integers = IntStream.range(0, 100).boxed(); |
特点
toArray
方法会返回对应的基本类型数组。(注意:虽然上面提到IntStream
可以存储Integer, Short, Char, Byte, Boolean
,但是仍然返回的是intt[]
)。- 产生可选结果的方法会返回一个
OptionalInt
、OptionalLong
或OptionalDouble
。这些方法与Optional
相似,但是具有getAsInt
,getAsLong
、getAsDouble
方法,而不是get
方法。 - 具有分别返回总和、平均值、最大值和最小值的sum、max和min方法。对象流没有定义这些方法。
summaryStatistics
方法会产生一个类型为IntSummaryStatistics
、LongSummaryStatistics
和DoubleSummaryStatistics
的对象,他们可以同hi报告流的总和、数量、平均值、最大值和最小值。
并行流
流时并行处理块操作变的容易。整个过程几乎是自动的。
创建并行流
Collection.parallelStream()
可以使用Collection.parallelStream()
方法从任何集合中获取一个并行流。
如:
1 | Stream<String> parallelWords = words.parallelStream(); |
stream.parallel()
对于现有的流,可以通过stream.parallel()
来获取一个并行流。
如:
1 | Stream<String> parallelWords = Stream.of(wordArray).parallel(); |
相关注意点
只要在终结方法执行时流处于并行模式,所有的中间流操作都将被并行化。
只要是并行就会存在竞态的问题。
如下面的例子就是有问题的:
1 | var shortWords = new int[12]; |
因为shortWords
会被传递到多个线程中,这就会存在竞态的问题,所以不能得到预期的结果。
排序并不排斥高效的并行处理。例如,当计算Stream.map(fun)
时,流可以被划分为n部分,它们会被并行的处理。然后结果会按照顺序重新组装起来。
stream.unordered
当然,如果不需要排序的需求,并行操作的性能将会得到很大的提升。此时可以通过stream.unordered
方法来标记对顺序没有要求。
例如stream.instinct
就会从中获得更高效率,原因是不用考虑哪一个相同的元素将会被保留。
TIPS
- 并行化会导致大量的开销,只有面对非常大的数据集才划算。
- 只有在底层的数据源可以被有效地分割为多个部分时,将流并行化才有意义。
- 并行流使用的线程池可能会因诸如文件I/O或者网络访问这样的操作被阻塞而饿死。
只有面对海量的内存数据和运算密集处理,并行流才会工作最佳。