1.从迭代到流的操作
在处理集合时,我们通常会迭代遍历它的元素,并在每个元素上执行某项操作。例如,假设我们想要对某本书的所有长单词进行计数。首先,将所有单词放到一个列表中:
String contents=new String(File.readAllBytes( Paths.get("alice.txt")),StandardCharsets.UTF_8);// Read file into string List<String> words=Arrays.asList(contents.split("\PL+")); // Split into words; nonletters are delimiters
现在,我们可以迭代它了:
long count=0; for(String w: words){ if(w.length()>12) count++; }
在使用流时,相同的操作看起来像下面这样:
long count=words.stream() .filter(w -> w.length() >12) .count();
流的版本比循环版本更易于阅读,因为我们不必扫描整个代码去查找过滤和计数操作,方法名就可以直接告诉我们其代码意欲何为。而且,循环需要非常详细地指定操作的顺序,而流却能够以其想要的任何方式来调度这些操作,只要结果是正确的即可。
流遵循了“做什么而非怎么做”的原则。在流的示例中,我们描述了需要做什么:获取长单词,并对它们计数。我们没有指定该操作应该以什么顺序或者哪个线程中执行。相比之下,本节开头处的循环要确切指定计算应该如何工作,因此也就丧失了进行优化的机会。
流表面上看起来和集合很类似,都可以让我们转换和获取数据。但是,它们之间存在着显著的差异:
1)流并不存储其元素。这些元素可能存储在底层的集合中,或者是按需生成的。
2)流的操作不会改变其数据源。例如,filter方法不会从新的流中移除元素,而是会生成一个新的流,其中不包括被过滤掉的元素。
3)流的操作是尽可能地惰性执行的。这意味着直至需要结果时,操作才会执行。例如,如果我们只想查看前5个长单词而不是所有长单词,那么filter方法就会在匹配到第5个单词后停止过滤。因此,我们甚至可以操作无限流。
stream和paralellelStream方法会产生一个用于words列表的stream。filter方法会返回另一个流,其中包括长度大于12的单词。count方法会将这个流化简为一个结果。
count=words.parallelStream().filter(w->w.length() >12).count();
这个工作流是操作流时的典型流程。我们建立了一个包括三个阶段的操作管道:
1)创建一个流。
2)指定将初始流转换为其他流的中间操作,可能包含多个步骤。
3)应用终止操作,从而产生结果。这个操作会强制执行之前的惰性操作。从此之后,这个流就再也不能用了。
java.util.stream.Stream<T>:
Stream<T> filter(Predicate<? super T> p)
产生一个流,其中包含当前流中满足P的所有元素。
long count()
产生当前流中元素的数量。这是一个终止操作。
java.util.Collection<E>:
default Stream<E> stream()
default Stream<E> parallelStream()
产生当前集合中所有元素的顺序流或并行流。
2.流的创建
如果你有一个数组,那么可以使用静态的Stream.of方法。
Stream<String> words=Stream.of(contents.split("\PL+")); // split returns a String[] array
of方法具有可变长参数,因此我们可以构建具有任意数量引元的流:
Stream<String> song=Stream.of("gently","down","the","stream");
使用Array.stream(array,from,to)可以从数组中位于from(包括)和to(不包括)的元素中创建一个流。
为了创建不包含任何元素的流,可以使用静态的Stream.empty方法:
Stream<String> silence=Stream.empty(); //Generic trype <String> is inferred; same as Stream.<String>empty()
Stream接口有两个用于创建无限流的静态方法。generate方法会接受一个不包含任何引元的函数(或者从技术上讲,是一个Supplier<T>接口的对象)。无论何时,只要需要一个流类型的值,该函数就会被调用以产生这样的值。我们可以像下面这样获得一个常量值的流:
Stream<String> echos=Stream.generate(() -> "Echo");
或者像下面这样获取一个随机数的流:
Stream<Double> randoms=Stream.generate(Math::random);
为了产生无限序列,例如0 1 2 3 ...,可以使用iterate方法。它会接受一个“种子”值,以及一个函数(从技术上讲,是一个UnaryOperation<T>),并且会反复地将该函数应用到之前的结果上。例如,
Stream<BigInteger> integers=Stream.iterate(BigInterger.ZERO,n -> n.add(BigInteger.ONE));
该序列中的第一个元素是种子BigInterger.ZERO,第二个元素是f(seed),即1(所为大整数),下一个元素f(f(seed)),即2,后续以此类推。
注:JavaAPI中有大量方法都可以产生流。例如,Pattern类有一个splitAsStream方法,它会按照某个正则表达式来分割一个CharSequence对象。可以使用下面的语句来将一个字符串分割为一个个的单词:
Stream<String> words=Pattern.compile("\PL+").splitAsStream(contents);
静态的Files.lines方法会返回一个包含了文件中所有行的Stream:
try(Stream<String> lines=Files.lines(path)){ Process lines }
java.util.stream.Stream:
static <T> Stream<T> of(T... values)
产生一个元素为给定值的流。
static <T> Stream<T> empty()
产生一个不包含元素的流。
static <T> Stream<T> generate(Supplier<T> s)
产生一个无限流,它的值是通过反复调用函数s而构建的。
static <T> Stream<T> iterate(T seed,UnaryOperator<T> f)
产生一个无限流,它的元素包含种子,在种子上调用f产生的值,在前一个元素上调用f产生的值,等等。
java.util.Arrays
static <T> Stream<T> stream(T[] array,int startInclusive,int endExclusive)
产生一个流,它的元素是由数组中指定范围内的元素构成的。
java.util.regex.Pattern
Stream<String> splitAsStream(CharSequence input)
产生一个流,它的元素是输入中由该模式界定的部分。
java.nio.file.Files
static Stream<String> lines(Path path)
static Stream<String> lines(Path path,Charset cs)
产生一个流,它的元素是指定文件中的行,该文件的字符集为UTF-8,或者为指定的字符集。
java.util.function.Supplier<T>
T get()
提供一个值。
3.filter,map和flatMap方法
流的转换会产生一个新的流,它的元素派生自另一个流中的元素。
下面,我们将一个字符串流转换为了只包含长单词的另一个流:
List<String> wordList=...;
Stream<String> longWords=wordList.stream().filter(w -> w.length() >12);
filter的引元是Predicate<T>,即从T到boolean的函数。
通常,我们想要按照某种方式来转换流中的值,此时,可以使用map方法并传递只执行该转换的函数。例如,我们可以像下面这样将所有单词都转换为小写。
Stream<String> lowercaseWords=words.stream().map(String::toLowerCase);
这里,我们使用的是带有方法引用的map,但是,通常我们可以使用lambda表达式来代替:
Stream<String> firstLetters=words.stream().map(s -> s.substring(0,1));
上面语句所产生的流中包含了所有单词的首字母。
在使用map时,会有一个函数应用到每个元素上,并且其结果是包含了应用该函数后所产生的所有结果的流。现在假设我们有一个函数,它返回的不是一个值,而是一个包含众多值的流:
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"]。
假设我们在一个字符串流上映射letters方法:
Stream<Stream<String>> result=words.stream().map(w -> letters(w));
那么就会得到一个包含流的流,就像[...["y","o","u","r"],["b","o","a","t"],...]。
为了将其摊平为字母流[..."y","o","u","r","b","o","a","t",...],可以使用flatMap方法而不是map方法:
Stream<String> flatResult=words.stream().flatMap(w ->letters(w)); //Calls letters on each and flattens the results
注:在流之外的库你也会发现flatMap方法,因为它是计算机科学中的一种通用概念。假设我们有一个泛型G(例如Stream),以及将某种类型T转换为G<U>的函数f和将类型U转换为G<V>的函数g。然后,我们可以通过使用flatMap来组合它们,即首先应用f,然后应用g。这是单子论的关键概念。但是不必担心,我们无需了解任何有关单子的知识就可以使用flatMap。
java.util.stream.Stream:
Stream<T> filter(Predicate<? super T> predicate)
产生一个流,它包含当前流中所有满足断言条件的元素。
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
产生一个流,它包含将mapper应用与当前流中所有元素所产生的结果。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
产生一个流,它是通过将mapper应用于当前流中所有元素所产生的结果连接到一起而获得的。(注意,这里的每个结果都是一个流)。
4.抽取子流和连接流
调用stream.limit(n)会返回一个新的流,它在n个元素之后结束(如果原来的流更短,那么就会在流结束时结束)。这个方法对于裁剪无限流的尺寸会显得特别有用。例如:
Stream<Double> randoms=Stream.generate(Math::random).limit(100);
会产生一个包含100个随机数的流。
调用stream.skip(n)正好相反:它会丢弃前n个元素。这个方法在将文本分隔为单词时会显得很方便,因为按照split方法的工作方式,第一个元素是没什么用的空字符串,我们可以通过调用skip来跳过它:
Stream<String> words=Stream.of(contents.split("\PL+")).skip(1);
我们可以用Stream类的静态的concat方法将两个流连接起来:
Stream<String> combined=Stream.concat( letters("Hello"),letters("World")); //Yields the stream ["H","e","l","l","o","W","o","r","l","d"]
当然,第一个流不应该是无限的,否则第二个流永远都不会得到处理的机会。
java.util.stream.Stream
Stream<T> limit(long maxSize)
产生一个流,其中包含了当前流中最初的maxSize个元素。
Stream<T> skip(long n)
产生一个流,它的元素是当前流中除了前n个元素之外的所有元素。
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
产生一个流,它的元素是a的元素后面跟着b的元素。
5.其他的流转换
distinct方法会返回一个流,它的元素是从原有流中产生,即原来的元素按照同样的顺序剔除重复元素后产生的。这个流显然能够记住它已经看到过的元素。
Stream<String> uniqueWords =Stream.of("merrily","merrily","merrily","gently").distinct(); //Only one "merrily is retained
对于流的排序,有多种sorted方法的变体可用。其中一种用于操作Comparable元素的流,而另一种可以接受一个Comparator。下面,我们对字符串排序,使得最长的字符串排在最前面:
Stream<String> longestFirst =
words.stream().sorted(Comparator.comparing(String::length).reversed());
与所有流转换一样,sorted方法会产生一个新的流,它的元素是原有流中按照顺序排列的元素。
当然,我们在对集合排序时可以不使用流。但是,当排序处理是流管道的一部分时,sorted方法就会显得很有用。
最后,peek方法会产生另一个流,它的元素与原来流中的元素相同,但是在每次获取一个元素时,都会调用一个函数。这对于调试来说很方便:
Object[] powers=Stream.iterate(1.0,p ->p*2) .peek(e -> System.out.println("Fetching " +e )) .limit(20).toArray();
当实际访问一个元素时,就会打印出来一条信息。通过这种方式,你可以验证iterate返回的无限流是被惰性处理的。
对于调试,你可以让peek调用一个你设置了断点的方法。
java.util.stream.Stream
Stream<T> distinct()
产生一个流,包含当前流中所有不同的元素
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)
产生一个流,它的元素是当前流中的所有元素按照顺序排列的第一个方法要求元素是实现了Comparable的类的实例。
Stream<T> peek(Consumer<? super T> action)
产生一个流,它与当前流中的元素相同,在获取其中每个元素时,会将其传递给action。
6.简单约简
现在你已经看到了如何创建和转换流,我们终于可以讨论最重要的内容了,即从流数据中获得答案。我们在本节所讨论的方法被称为约简。约简是一种终结操作(terminal operation),它们会将流约简为可以在程序中使用的非流值。
你已经看到过一种简单的约简,count方法会返回流中元素的数量。
其他的简单约简还有max和min,它们会返回最大值和最小值。这里稍作解释,这些方法返回的是一个类型Optional<T>的值,它要么在其中包装了答案,要么表示没有任何值(因为流碰巧为空)。在过去,碰到这种情况返回null是很常见的,但是这样做会导致在未做完备测试的程序中产生空指针异常。Optional类型是一种更好的表示缺少返回值的方式。
下面展示了可以如何获得流中的最大值:
Optional<String> largest=words.max(String::compareToIgnoreCase);
System.out.println("largest: " + largest.orElse(""));
findFirst返回的是非空集合中的第一个值。它通常会在与filter组合使用时显得很有用。例如,下面展示了如何找到第一个以字母Q开头的单词,前提是存在这样的单词:
Optional<String> startWithQ =words.filter(s -> s.startWith("Q")).findFirst();
如果不强调使用第一个匹配,而是使用任意的匹配都可以,那么就可以使用findAny方法。这个方法在并行处理流时会很有效,因为流可以报告任何它找到的匹配而不是被限制为必须报告的第一个匹配。
Optional<String> startWithQ=words.parallel().filter(s -> s.startWith("Q“)).findAny();
如果只想知道是否存在匹配,那么可以使用anyMatch。这个方法会接受一个断言引元,因此不需要使用filter。
boolean aWordStartsWithQ = words.parallel().anyMatch( s-> s.startWith("Q”));
还有allMatch和noneMatch方法。它们分别会在所有元素和没有任何元素匹配断言的情况下返回true。这些方法也可以通过并行运行而获益。
java.util.stream.Stream:
Optional<T> max(Comprator<? super T> comparator)
Optional<T> min(Comprator<? super T> comparator)
分别产生这个流的最大元素和最小元素,使用由给定比较器定义的排序规则,如果这个流为空,会产生一个空的Optional对象。这些操作都是终结操作。
Optional<T> findFirst()
Optional<T> findAny()
分别产生这个流的第一个和任意一个元素,如果这个流为空,会产生一个空的Optional对象。这些操作都是终结操作。
boolean anyMatch(Predicate<? super T> predicate)
boolean allMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)
分别在这个流中任意元素,所有元素和没有任何元素匹配给定断言时返回true。这些操作都是终结操作。
7.Optional类型
Optional<T>对象是一种包装器对象,要么包装了类型T的对象,要么没有包装任何对象。对于第一种情况,我们称为这种值是存在的。Optional<T>类型被当做一种更安全的方式,用来替代类型T的引用,这种引用要么引用某个对象,要么为null。但是,它只有在正确使用的情况下才会更安全。
1)如何使用Optional值
有效地使用Optional的关键是要使用这样的方法:它在值不存在的情况下会产生一个可替代物,而只有在值存在的情况下才会使用这个值。
让我们来看第一条策略。通常,在没有任何匹配时,我们会希望使用某种默认值,可能是空字符串:
String result=optionalString.orElse(""); // The wrapped string, or "" if none
你还可以调用代码来计算默认值:
String result=optionalString.orElseGet(() -> Locale.getDefault().getDisplayName()); //The function is only called when needed
或者可以在没有任何值时抛出异常:
String result=optionalString.orElseThrow(IllegalStateException::new); //Supply a method that yields an exception object
你刚刚看到了如何在不存在任何值的情况下产生相应的替代物。另一条使用可选值的策略是只有在其存在的情况下才消费该值。
ifPresent方法会接受一个函数。如果该可选值存在,那么它会被传递给该函数。否则,不会发生任何事情。
optionalValue.ifPresent(v -> Process v);
例如,如果在该值存在的情况下想要将其添加到某个集中,那么就可以调用
optionalValue.ifPresent(v -> results.add(v));
或者直接调用
optionalValue.ifPresent(results::add);
当调用ifPresent时,从该函数不会返回任何值。如果想要处理函数的结果,应该使用map:
Optional<Boolean> added=optionalValue.map(results::add);
现在added具有三种值之一:在optionalValue存在的情况下包装在Optional中的true或false,以及在optionalValue不存在的情况下空Optional。
注:这个map方法与之前描述的Stream接口的map方法类似。你可以直接将可选值想象成尺寸为0或1的流。结果的尺寸也是0或1,并且在后一种情况中,会应用到函数。
java.util.Optional
T orElse(T other)
产生这个Optional的值,或者在该Optional为空时,产生other。
T orElseGet(Supplier<? extends T> other)
产生这个Optional的值,或者在该Optional为空时,产生调用other的结果。
<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)
产生这个Optional的值,或者在该Optional为空时,抛出调用exceptionSupplier的结果。
void ifPresent(Consumer<? super T> consumer)
如果该Optional不为空,那么就将它的值传递给consumer。
<U> Optional<U> map(Function<? super T,? extends U> mapper)
产生将该Optional的值传递给mapper后的结果,只要这个Optional不为空且结果不为null,否则产生一个空Optional。
2)不适合使用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();
3)创建Optional值
如果想要编写方法来创建Optional对象,那么有多个方法可以用于此目的,包括Optional.of(result)和Optional.empty()。例如:
public static Optional<Double> inverse(Double x){ return x==0 ? Optional.empty() : Optional.of(1/x);
ofNullable方法被用来作为可能出现的null值和可选值之间的桥梁。Opitonal.ofNullable(obj)会在obj不为null的情况下返回Optional.of(obj),否则会返回Optional.empty()。
java.util.Optional
static <T> Optional<T> of(T value)
static <T> Optional<T> ofNullable(T value)
产生一个具有给定值的Optional。如果value为null,那么第一个方法会抛出一个NullPointerException对象,而第二个方法会产生一个空Optional。
static <T> Optional<T> empty()
产生一个空Optional。
4)用flatMap来构建Optional值的函数
假设你有一个可以产生Optional<T>对象的方法f,并且目标类型T具有一个可以产生Optional<U>对象的方法g。如果它们都是普通的方法,那么你可以通过调用s.f().g()来将它们组合起来。但是这种组合没法工作,因为s.f()的类型为Optional<T>,而不是T。因此,需要调用:
Optional<U> result=s.f().flatMap(T::g);
如果s.f()的值存在,那么g就可以应用到它上面。否则,就会返回一个空Optional<U>。
很显然,如果有更多的可以产生Optional值的方法或Lambda表达式,那么就可以重复此过程。你可以直接将对flatMap的调用链接起来,从而构建由这些步骤构成的管道,只有所有步骤都成功时,该管道才会成功。
例如,考虑前一节安全的inverse方法。假设我们还有一个安全的平方根:
public static Optional<Double> squareRoot(Double x){ return x < 0 ? Optional.empty() : Optional.of(Math.sqrt(x));
}
那么你可以像下面这样计算倒数的平方根了:
Optional<Double> result=inverse(x).flatMap(MyMath::squareRoot);
或者,你可以选择下面的方式:
Optional<Double> result=Optional.of(-4,0).flatMap(MyMath::inverse).flatMap(MyMath::squareRoot);
无论是inverse方法还是squareRoot方法返回Optional.empty(),整个结果都会为空。
注:你已经在Stream接口中看到过flatMap方法,当时这个方法被用来将可以产生流的两个方法组合起来,其实现方式是摊平由流构成的流。如果将可选值当做尺寸为0和1的流来解释,那么Optional.flatMap方法与其操作方式一样。
java.util.Optional
<U> Optional<U> flatMap(Function<? suer T, Optional<U>> mapper)
产生将mapper应用于当前的Optional值所产生的结果,或者在当前Optional为空时,返回一个空Optional。
8.收集结果
当处理完流之后,通常会想要看其元素。此时可以调用iterator方法,它会产生可以用来访问元素的旧式风格的迭代器。
或者,可以调用forEach方法,将某个函数应用于每个元素:
stream.forEach(System.out::println);
在并行流上,forEach方法会以任意顺序遍历各个元素。如果想要按照流中的顺序来处理它们,可以调用forEachOrdered方法。
可以调用toArray获得由流的元素构成的数组。
因为无法在运行时创建泛型数组,所以表达式stream.toArray()会返回一个Object[]数组。如果想要让数组具有正确的类型,可以将其传递到数组构造器中:
String[] result=stream.toArray(String[]::new); //stream.toArray() has type Object[]
针对将流中的元素收集到另一目标中,有一个便捷方法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 averageWordLength=summary.getAverage(); double maxWordLength=summary.getMax();
9.收集到映射表中
假设我们有一个Stream<Person>,并且想要将其元素收集到一个映射表中,这样后续就可以通过它们的ID来查找人员了。Collectors.toMap方法有两个函数引元,它们用来产生映射表的键和值。例如,
Map<Integer,String> idToName=people.collect(
Collectors.toMap(Person::getID,Person::getName));
在通常情况下,值应该是实际的元素,因此第二个函数可以使用Function.indentity()。
Map<Integer,Person> idToName=people.collect(
Collectors.toMap(Person::getId,Function.identity()));
如果有多个元素具有相同的键,那么就会存在冲突,收集器将会抛出一个IllegalStateException对象。可以通过提供第3个函数引元来覆盖这种行为,该函数会针对给定的已有值和新值来解决冲突并确定对应的值。这个函数应该返回已有值,新值或它们的组合。
Stream<Locale> locales=Stream.of(Locale.getAvailableLocales()); Map<String,String> languageNames=locales.collect( Collectors.toMap( Locale::getDisplayLanguage, l -> l.getDisplayLanguage(l), (existingValue,newValue) -> existingValue));
现在,假设我们想要了解给定国家的所有语言,这样我们就需要一个Map<String,Set<String>>。例如,“Switzerland”的值是集[French,German,Italian]。首先,我们为每种语言都存储一个单例集。无论何时,只要找到了给定国家的新语言,我们都会将已有集和新集做并操作。
Map<String,Set<String>> countryLanguageSets=locales.collect( Collectors.toMap( Locale::getDisplayCountry, l -> Collectors.singleton(l.getDisplayLanguage()), (a,b) -> { //Union of a and b Set<String> union=new HashSet<>(a); union.addAll(b); return union; }));
如果想要得到TreeMap,那么可以将构造器作为第4个引元来提供。你必须提供一种合并函数。现在它会产生一个TreeMap:
Map<Integer,Person> idToName=people.collect( Collectors.toMap( Person::getId, Function.identity(), (existingValue,newValue) -> { throw new IllegalStateException();} TreeMap::new));
注:对于每一个toMap方法,都有一个等价的可以产生并发映射表的toConcurrentMap方法。单个并发映射表可以用于并行集合处理。当使用并行流时,共享的映射表比合并映射表要更高效。注意,元素不再是按照流中的顺序收集的,但是通常这不会有什么问题。
10.群组和分区
在上一节中,你看到了如何收集给定国家的所有语言,但是其处理显得有些冗长。你必须为每个映射表的值都生成单例集,然后指定如何将现有集与新集合并。将具有相同特性的值群聚成组是非常常见的,并且groupingBy方法直接就支持它。
让我们来看看通过国家来群组Locale的问题。首先,构建该映射表:
Map<String,List<Locale>> countryToLocales =locales.collect(
Collectors.groupingBy(Locale::getCountry));
函数Locale::getCountry是群组的分类函数,你现在可以查找给定国家代码对应的所有地点了,例如:
List<Locale> swissLocales=countryToLocales.get("CH"); //Yieds locales [it_CH,de_CH,fr_CH]
当分类函数是断言函数(即返回boolean值的函数)时,流的元素可以分区为两个列表:该函数返回true的元素和其他的元素。在这种情况下,使用partitioningBy比使用groupingBy要更高效。例如,在下面的代码中,我们将所有Locale分成了使用英语和使用所有其他语言的两类:
Map<Boolean,List<Locale>> englishAndOtherLocales =locales.collect( Collectors.partitioningBy(l -> l.getLanguage().equals("en"))); List<Locale> englishLocales=englishAndOtherLocales.get(true);
注:如果调用groupingByConcurrent方法,就会使用并行流时获得一个被并行组装的并行映射表。这与toConcurrentMap方法完全类似。
11.下游收集器
groupingBy方法会产生一个映射表,它的每个值都是一个列表。如果想要以某种方式来处理这些列表,就需要提供一个“下游收集器”。
例如,如果想要获得集而不是列表,那么可以使用上一节中看到的Collector.toSet收集器:
Map<String,Set<Locale>> countryToLocaleSet=locales.collect(
groupingBy(Locale::getCountry,toSet()));
12.约简操作
java.util.Stream:
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity,BinaryOperator<T> accumulator)
<U> U reduce(U identity,BiFunction<U,? super T,U> accumulator,BinaryOperator<U> combiner)
用给定的accumulator函数产生流中元素的累积总和。如果提供了幺元,那么第一个被累计的元素就是该幺元。如果提供了组合器,那么它可以用来将分别累积的各个部分整合成总和。
13.基本类型流
使用包装器是很低效的。流库中具有专门的类IntStream,LongStream和DoubleStream,用来直接存储基本类型值,而无需使用包装器。
14.并行流
流使得并行处理块操作变得很容易。这个过程几乎是自动的,但是需要一些规则。
首先,必须有一个并行流。可以使用Collection.parallelStream()方法从任何集合中获取一个并行流:
Stream<String> parallelWords=words.parallelStream();
而且,parallel方法可以将任意的顺序流转换为并行流。
Stream<String> parallelWords=Stream.of(wordArray).parallel();
只要在终结方法执行时,流处于并行模式,那么所有的中间流操作都将被并行化。
当流操作并行运行时,其目标是要让其返回结果与顺序执行时返回的结果相同。重要的是,这些操作可以以任意顺序执行。
假设你想要对字符串流中的所有短单词计数:
int[] shortWords=new int[12]; words.parallelStream().forEach( s ->{ if(s.length<12) shortWords[s.length()]++;}); //Error-race condition! System.out.println(Arrays.toString(shortWords));
这是一种非常非常糟糕的代码。传递给forEach的函数会在多个并发线程中运行,每个都会更新共享的数组,这是一种经典的竞争情况。
你的职责是要确保传递给并行流操作的任何函数都可以安全地并行执行,达到这个目的的最佳方式是远离易变状态。在本例中,如果用长度将字符串群组,然后分别对它们进行计数,那么就可以安全地并行化这项运算。
Map<Integer,Long> shortWordCounts= words.parallelStream() .filter(s ->s.length() <10) .collect(groupingBy( String::length, counting()));
注:传递给并行流操作的函数不应该被堵塞。并行流使用fork-join池来操作流的各个部分。如果多个流操作被阻塞,那么池可能就无法做任何事情了。
默认情况下,从有序集合(数组和列表),范围,生成器和迭代产生的流,或者通过调用Stream.sorted产生的流,都是有序的。它们的结果是按照原来元素的顺序积累的,因此是完全可预知的。如果运行相同的操作两次,将会得到完全相同的结果。
排序并不排斥高效的并行处理。例如,当计算stream.map(fun)时,流可以被划分为n的部分,它们会被并行地处理。然后,结果将会按照顺序重新组装起来。
当放弃排序需求时,有些操作可以被更有效地并行化。