流提供了一种让我们可以在比集合更高的概念级别上指定计算的数据试图,用来解决“做什么而非怎么做”的问题。
从迭代到流的操作
eg:统计某本书的长单词数量
String contents = new String(Files.readAllBytes(Path.get("alice.txt")),StandardCharsets.UTF-8):
List<String> words = Arrays.asList(contents.split("\\PL+"));
//使用迭代
long count = 0;
for(String w : words){
if(w.length()>12)
count++;
}
//使用流操作
long count = words.stream().filter(w->w.length()>12).count();
流的版本比循环更易于阅读。*
仅将stream改为parallelStream就可以让流库以并行方式执行过滤和计数。
long count = word.parallelStream().filter(w->w.length()>12).count();
流表面上看起来和集合很类似,都可以转换和获取数据。但是,它们之间存在着显著的差异:
- 流并不存储元素
- 流操作不修改数据源
- 流操作是尽可能惰性执行
流的创建
- 可以使用Collection接口的stream方法将任何集合转换为一个流。
- 如果有一个数组,那么可以使用静态的Stream.of方法:
Stream<String> words = Stream.of(contents.split("\PL+");
//split returns a string[] arrary
of方法具有可变长参数,因此可以构建具有任意数量引元的流:
Stream<String> song = Stream.of("gently","down","the","stream");
使用Array.stream(array,from,to)可以从数组中位于from 和to(不包含)的元素中创建一个流。 - 为了创建不包含任何元素的流,可以使用静态的Stream.empty方法:
Stream<String> silence = Stream.empty(); - Stream 接口有两个用于创建无限流的静态方法。generate方法会接受一个不包含任何引元的函数(或者是一个Suplier<T>接口的对象)。无论何时,只需要一个流类型的值,该函数就会被调用产生一个这样的值。
Stream<String> echos = Stream.generate(()->"Echo");
或者像下面这样获取一个随机数流:
Stream<Double> randoms = Stream.generate(Math::random);
为了产生无限序列,可以使用iterate方法。它会接受一个种子值,以及一个函数(从技术上讲,是一个UnaryOperation<T>),并且会反复将该函数应用到之前的结果上。
Stream<BigInteger> integers
= Stream.iterate(BigInteger.ZERO,n->add(BigInteger.ONE));
filter 、map和flatMap方法
流的转换会产生一个新的流,它的元素派生自另一个流中的元素。
filter转换会产生一个流,它的元素与某种条件相匹配。我们可以将一个字符串流转换为只包含长单词的另一个流:
List<Strig> wordList = ...;
Stream<String> longwords = wordList.stream().filter(w->w.leng()>12);
filter的引元是Predicate<T>,即从T到boolean的函数。
通常按照某种方式转换流中的值,可以使用map方法传递执行某转换函数。例如将所有单词都转换为小写:
Stream<String> lowercaseWords = words.stream().map(String::toLowerCase);
上面使用的是带有方法引用的map,但是通常可以使用lambda表达式来代替:
Stream<String> lowercaseWords = words.stream().map(s->s.substring(0,1));
上面语句产生的流中包含了所有单词的首字母。
使用map时,会有一个函数应用到每一个元素上,并且其结果是包含了应用该函数后所产生的所有结果的流。
将某种类型T转换为泛型G<U>和将类型U转换为G<V>,可以通过使用flatMap组合他们。
即public static Stream<String> letters(String s){
List<String> result = new ArrayList<>();
for(int i=0; i< s.length();i++){
result.add(s.substring(i,i+1));
}
return result.stream();
}
letters("boat")的返回值是["b","o","a","t"];
Stream<Stream<String>> result = words.stram().map(w->letters(w);
将会得到一个包含流的流,就像[...[..],[..],[..]...],为了将其摊平为字母流[.......],可以使用flatMap方法,而不是map方法。
1.4 抽取子流和连接流
调用stream.limit(n)会返回一个新的流。它在N个元素之后结束。这个方法对于裁剪无限流的尺寸会显得很御用。
eg:
Stream<Double> randoms = Stream.generate(Math::random).limit(100);
会产生一个包含100个随机数的流。
调用stream.skip(n)正好相反:它会丢弃前n个元素。
可以用Stream类的静态的concat方法将两个流连接起来:
Stream<String> combined = Stream.concat(letters("hello),letters("world"));
当然第一个流不应该是无限的,否则第二个流永远得不到处理的机会。
其他流的转换
distinct方法会返回一个流,它的元素是从原有流中产生的,即原来的元素按照同样的顺序提出重复元素之后产生的。这个流显然能够记住它已经看过的元素。
Stream<String> uniqueWords = Stream.of("merrily","merrily","merrily","gently").distinct();
流排序,有多种sorted方法可用。其中一种用于操作Comparable元素的流,而另一种可以接受一个Comparator。下面,我们对字符串排序,使得最长的字符串排在最前面。
Stream<String> longestFirst = words.stream().sorted(Comparator.comparing(String::length).reversed());
与所有的流转化一样,sorted方法会产生一个新的流,它的元素是原有流中按照顺序排列的元素。
当然,我们在对集合排序时可以不使用流。但是,当排序处理是流管道的一部分时,sorted方法就会显得很有用。
peek方法会产生另一个流,它的元素与原来流中的元素相同,但是每次获取一个元素时,都会调用一个函数。
简单约简
约简:一种终结操作,将流约简为可以在程序中使用的非流值。
- count方法会返回流中元素的数量。
- max和min方法会返回最大值和最小值。
- findFirst方法返回的是非空集合中的第一个值。通常会在与filter组合使用时显得很有用。
例如找到第一个以字母Q开头的单词:
Optional<String> startsWithQ = words.filters(s->s.startsWith("Q")).findFirst();
findAny()可以用于查找任意一个符合条件的元素。
如果只想知道是否存在匹配,可以使用anyMatch()。这个方法会接受一个断言引元,因此不需要使用filter。
还有allMatch和noneMatch方法,他们分别会在所有元素和没有任何元素匹配断言的情况下返回true。这些方法可以通过并行运行而获益。
Optional类型
Optional<T>对象是一种包装器对象,要么包装了类型T的对象,要么没有包装任何对象。对于第一种情况,我们称这种值为存在的。Option<T>类型被当作是一种更安全的方式,用来替代类型T的引用,要么引用某个对象,要么为Null。但是,它只有在正确使用的情况下才安全。
如何使用Optional值
有效地关键要这样使用:它在值不存在的情况下会产生一个可替代物,而只有在值存在的情况下才会使用这个值。
第一条策略,通常没有任何匹配是,我们会希望使用某种默认值,可能是空字符串:
String result = optionalString.orElse("");
//The wrapped string, or "" if none.
可以调用代码计算默认值:
String result = optionalString.orElseGet(()->Local.getDefault.getDisplayName());
// The function is only called when needed;
或者可以在没有任何值时抛出异常:
String result = optionalString.orElseThrow(IllegalStateException::new);
//supply a method that yields an exception object
另一条策略,只有在其存在的情况下才消费该值。
ifPresent方法会接受一个函数。如果该可选值存在,那么它会被传递给该函数。否则不会发生任何事情。
optionValue.ifPresent(v-> Process v);
例如,如果在该值存在的情况下想要添加到某个集中,那么就可以调用optionalValue.ifPresent(v->results.add(v));
或者直接调用
optionalValue.ifPresent(result::add);
当调用ifPresent时,从该函数不会返回任何值。如果想要处理函数的结果,应该使用map:
Optional<Boolean> added = optionalValue.map(results::add);
不适合使用Optional值的方式
如果没有正确地使用Optional值,那么相比较以往的得到“某物或null”的方式,你并没有得到任何好处。
get方法会在Optional值存在的情况下获得其中包装的元素,或者在不存在的情况下抛出一个NoSuchElementException对象。因此,
Optional<T> optionalValue = ...;
optionalValue.get().someMethod();
并不比下面的方式更安全:
T value = ... ;
value.someMethod();
isPresent方法会报告某个Optional<T>对象是否具有一个值。但是
if(optionalValue.isPresent())
optionalValue.get().someMethod();
并不比下面的方式更安全:
if (value != null ) value.someMethod();
1.7.3 创建Optional值
Optional.of(result)
Optional.empty()
Optional.ofNullable()方法被用来作为可能出现的null值和可选值之间的桥梁。
用flatMap来构建Optional值的函数
收集结果
处理完流后,可以调用iterator方法,会产生可以用来访问元素的旧式风格的迭代器。
或者可以调用forEach方法,将某个函数应用于每个元素:
stream.forEach(System.out.println);
在并行流上,forEach方法会以任意顺序遍历各个元素。如果想要按照流中的顺序来处理,可以调用forEachOrdered方法。当然,这个方法会丧失并行处理的部分甚至全部优势。
更常见的情况是将结果收集到数据结构中。此时,可以调用toArray获得由流的元素构成的数组。
因为无法在运行时创建泛型数组,所以表达式stream.toArray()会返回一个Object[]数组。
如果 想要数组具有正确的类型,可以将其传递到数组构造器中:
String[] result = stream.toArray(String[]::new);
针对流中的元素收集到另一个目标中,有一个便捷方法collect可用,它会接受一个Collector接口的实例。Collectors类提供了大量用于生成公共收集器的工厂方法。为了将流收集到列表或集中,可以直接调用
List<String> result = stream.collect(Collectors.toList());
或
Set<String> result = stream.collect(Collectors.toSet());
如果想要控制获得的集的种类,可以使用下面的调用:
TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new));
假设想要通过连接操作来收集流中的字符串,可以调用:
String result = stream.collect(Collectors.joining());
可以将分隔符传递给joining方法:
String result = stream.collect(Collectors.joining(","));
如果流中包含除字符串以外的其他对象,那么我们需要现将其转换为字符串,就像下面这样:
String result = stream.map(Object::toString).collect(Collectors.joining(","));
如果想要将流的结果约简为和、平均值、最大值和最小值,可以使用summarizing(Int|Long|Double)方法的某一个。这些方法会接受一个将流对象映射为数据的函数,同时,这些方法会产生类型为(Int|Long|Double)SummaryStatistics的结果,同时计算总和、数量、平均值和最大、最小值》
IntSummaryStatistics summary = stream.collect(Collectors.summarizingInt(String::length));
double averagedWordLength = summary.getAverage();
double maxWordLength = summary.getMax();
收集到映射表中
假设我们有一个Stream<Person>,并且想要将元素收集到一个映射表中,这样后续就可以通过ID查找人员了。Collectors.toMap方法有两个函数引元,用来产生映射表的键和值。
eg:
Map<Integer,String> idToName = people.collect(Collectors.toMap(Person::getId,Person::getName));
通常情况,值应该是实际的元素,因此第二个函数可以使用Function.identity()。
Map<Integer,Person> idToPerson = people.collect(Collectors.toMap(Person::getId,Function::identity()));
如果有多个 元素具有相同的键,就会存在冲突,收集器将会抛出一个IllegalStateException对象。可以通过提供第三个函数引元来覆盖这种行为,该函数会针对给定的已有值和新值来解决冲突并确定键对应的值。这个函数应该返回已有值,新值和他们的组合。
如果想要得到TreeMap,那么可以将构造器作为第4个引元来提供。必须提供一种合并函数。
Map<Integer,Person> idToPerson = people.collect(
Collectors.toMap(
Person::getId,
Function.identity(),
(existingValue,newValue)->{throw new IllgealStateException();},
TreeMap::new));
群组和分区
通过国家来群组Locale问题,构建映射表:
Map<String,List<Locale>> countryToLocales = locales.collect(
Collectors.groupingBy(Locale::getCountry));
函数Locale::getCountry是群组的分类函数
当分类函数是断言函数(即返回Boolean函数)时,流的元素可以分为两个列表:该函数返回true的元素和其它的元素。这种情况,使用partitioningBy比使用gourpingBy更高效。
下游收集器
groupingBy方法产生一个映射表,每个值是一个列表。如果要以某种方式处理这些列表,就需要提供一个“下游处理器”。
Java提供了多种可以将群组元素约简为数字的收集器:
counting会产生收集到元素的个数:
Map<String,Long> countryToLocaleCounts = locales.collect(groupingBy(Locale::getCountry, counting()));
summing(Int|Long|Double)会接受一个函数引元,将该函数应用到下游元素中,并产生他们的和。
Map<String,Integer> stateToCityPopulation = cities.collect(groupingBy(City::getState,summingInt(City::getPopulation)));
maxBy和minBy方法会接受一个比较器,并产生一个下游元素中的最大值和最小值。例如:
Map<String,Optional<City>> stateToLargestCity = cities.collect(groupingBy(City::getState,maxBy(Comparator.comparing(City::getPopulation))));
mapping方法会产生将函数应用到下游结果的收集器,并将函数值传递给另一个收集器。例如:
Map<String,Optional<String>> stateToLongestCityName = cities.collect(
groupingBy(City::getState,
mapping(City::getName,
maxBy(Comparator.comparing(String::length)))));
约简操作
reduce方法是一种用于从流中计算某个值的通用机制,其最简单的形式将接受一个二元函数,并从前两个元素开始持续应用它。
如果该函数是一个求和函数,就很容易解释这种机制:
List<Integer> values = ...;
Optional<Integer> sum = values.stream().reduce((x,y)->x+y);
基本流类型
IntStream/LongStream/DoubleStream
用来创建IntStream,需要调用IntStream.of和Arrays.stream方法:
IntStream stream = IntStream.of(1,1,2,3,5);
stream = Arrays.stream(values,from,to));
与对象流一样,可以使用静态的generate和iterate方法。此外,IntStream和LongStream有静态方法range和rangeClosed,可以生成步长为1的整数范围:
IntStream zeroToNinetyNine = IntStream.range(0,100);
IntStream zeroToHundred = IntStream.rangeClosed(0,100);
CharSequence接口拥有codePoints和chars方法,可以生成由字符的unicode码或由UTF-16编码机制的码元构成的IntStream。
String sentence ="\uD835\uDD46 is the set of octonions";
IntStream codes = sentence.codePoints();//The
当有一个对象流时,可以使用mapToInt,mapToLong,mapToDouble将其转换为基本类型流。
为了将基本类型流转换为对象流:需要使用boxed方法:
Stream<Integer> integers = IntStream.range(0,100).boxed;
并行流
流使得并行处理模块变得很容易。
可以用Collection.parallelStream()方法从任何集合中获取并行流。
Stream<String> parrllelWords = words.parallelStream();
parllel方法可以将任意顺序转换为并行流。
Stream<String> parrllelWords = Stream.of(wordArray).parrllel();
只要在终结方法执行时,流处于并行模式,那么所有的中间流操作都将被并行化。
未完待续。。。
End..