Java SE8的流库

作者: 小疯子 分类: Java,Java8 发布时间: 2019-02-18 09:26
学习《Java核心技术 卷2》记录
Java的流库,是在Java SE 8中引入的,用来以“做什么而非怎么做”的方式处理集合。
流提供了一种让我们可以在比集合更高的概念级别上指定计算的数据视图。

入门

一.  从迭代到流的操作

对所有的长单词进行计数,words是一个List<String>的单词集合
long count = 0;

for(String w: words){

if(w.length()>12) count++;

}
如果使用流的话,代码看起来如下
long count = words.stream().filter(w -> w.length > 12).count();

流的版本比循环版本更易于阅读,因为我们不需要扫描整个代码去查找过滤和计数操作,方法名就可以直接告诉我们其代码意欲何为。

让流库以并行方式执行过滤和计数,仅仅需要修改为parallelStream
long count = words.parallelStream().filter(w -> w.length > 12).count();
流和集合的差别:流并不存储其元素;流的操作不会修改其数据源;流的操作尽可能是惰性执行的,意味着直至需要结果时,操作才会执行。
以上的工作流是操作流时的典型例子。我们建立了一个包含三个阶段的操作管道:
1. 创建一个流;
2. 指定将初始流转换为其他流的中间操作,可能包含多个步骤;
3. 应用终止操作,从而产生结果。这个操作会强制执行之前的惰性操作。从此之后,这个流就再也不能用了。

二. 流的创建

前面看到了可以用Collection接口的stream方法将任何集合转换为一个流。如果你有一个数组,那么可以使用静态的Stream.of方法。
Stream<String> words = Stream.of(contents.split("\\PL+"));

//split返回的是String[] array
of方法具有可变长参数,因此可以构建具有任意数量引元的流。
Stream<String> words = Stream.of("gently","down","the");
使用Array.stream(array, from, to)可以从数组中位于from(包括)和to(不包括)的元素中创建一个流。
创建不包含任何元素的流,如下使用静态的empty方法。
Stream<String> words = Stream.empty();
Stream接口有两个用于创建无限流的静态方法,一个是generate,一个是iterate
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(BigInteger.ZERO, n -> n.add(BigInteger.ONE));
该序列中的第一个元素是种子BigInteger.ZERO,第二个元素是f(seed),即1(作为大整数),下一个元素是f(f(seed))即2,后续以此类推
注意: Java API中有大量的方法都可以产生流。例如Pattern类有一个splitAsStream方法,它会按照某个正则表达式来分割一个CharSequence.
Stream<String> words = Pattern.compile("\\PL+").splitAsStream(contents); //将一个字符串分割为一个个的单词。
静态的Files.Lines方法会返回一个包含了文件中所有行的Stream:
Stream<String> lines = Files.lines(path)

流的转换

三.  filter、map和flatMap方法

流的转换会产生一个新的流,它的元素派生自另一个流中的元素。我们已经看到了filter转换会产生一个流,它的元素与某种条件相匹配。
下面,将一个字符串流转换为了只包含长单词的另一个流:
List<String> wordList = ... ;

Stream<String> longWords = wordList.stream().filter(w -> w.length() > 12);
filter的引元是Predicate<T>, 即从T到boolean的函数。(Predicate是断定的意思)
通常,我们想要按照某种方式来转换流中的值,此时,可以使用map方法并传递执行该转换的函数。如下将所有单词都转换为小写。
Stream<String> lowercaseWords = words.stream().map(String::toLowerCase));
上面使用的是带有方法引用的map,但是通常我们可以使用lamba表达式代替,如下产生的流中包含了所有单词的首字母:
Stream<Stream> 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"],["b","o","a","t"]...]。为了将其摊平为字母流[..."y","o","u","b","o","a","t"..],可以使用flatMap方法而不是map方法:
Stream<String> result = words.stream().flatMap(w -> letters(w));
flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起

四. 抽取子流和连接流

调用stream.limit(n)会返回一个新的流,它在n个元素之后结束。这个方法对于裁剪无限流的尺寸会显得特别有用。
Stream<Double> randoms = Stream.generate(Math::random).limit(100); 会产生一个包含100个随机数的流。
调用stream.skip(n)正好相反:它会丢弃前n个元素。
Stream<String> words = Stream.of(contents.split("\\PL+")).skip(1); 丢弃第一个没什么用的空字符串
可以用静态的concat方法将两个流链接起来
Stream<String> words = Stream.concat(letters("Hello"),letters("World")); 第一个流不应该是无限的,否则第二个流永远都不会得到处理的机会

五. 其他的流转换

distinct方法返回一个流,它的元素是从原有流中产生的,即原来的元素按照同样的顺序剔除重复元素后产生的。
Stream<String> uniqueWords = Stream.of("merr","merr","merr","gently").distinct(); //仅仅有一个merr被留下了
对于流的排序,有多种sorted方法的变体可用。其中一种用于操作Comparable元素的流,而另一种可以接受一个Comparator。下面对字符串进行排序,使得最长的字符串排在最前面
Stream<String> longestFirst = words.stream().sorted(Comparator.comparing(String::length).reversed());
与所有的流转换一样,sorted方法会产生一个新的流,它的元素是原有流中按照顺序排列的元素。
最后peek方法会产生另一个流,它的元素和原来流中的元素相同,但是在每次获取一个元素时,都会调用一个函数。这对于调试来说很方便,peek 对每个元素执行操作并返回一个新的 Stream,对于调试,可以让peek调用一个你设置了断点的方法。
Object[] powers = Stream.iterate(1.0, p->p*2)

                                  .peek(e->System.out.println("Fetching" + e))

                                  .limit(20).toArray();

终止操作

六. 简单约简

约简是一种终结操作,他们会将流约简为可以在程序中使用的非流值。
已经看到过一种简单约简:count方法会返回流中元素的数量。
其他的简单约简还有max和min,返回最大值和最小值,这些方法返回的是一个类型Optional<T>的值,要么在其中包含了答案,要么表示没有任何值(因为流碰巧为空)
下面展示了可以如何获得流中的最大值:
Optional<String> largest = words.max(String::compareToIgnoreCase);

System.out.println("largest:"+largest.ofElse(""));
findFirst返回的是非空集合中的第一个值。它通常在与filter组合使用时显得很有用。
Optional<String> startsWithQ = words.filter(s -> s.startWith("Q")).findFirst(); //如何找到第一个以字母Q开头的单词,前提是存在这样的单词
如果不强调使用第一个匹配,而是使用任意的匹配都可以,就可以使用findAny方法。这个方法在并行处理流时会很有效,因为流可以报告任何它找到的匹配而不是被限制为必须报告的第一个匹配:
Optional<String> startsWithQ = words.parallel().filter(s -> s.startWith("Q")).findAny();
如果只想知道是否存在匹配,那么可以使用anyMatch。这个方法会接受一个断言引元,所以不需要使用filter
boolean aWordStartsWithQ = words.parallel().anyMatch(s -> s.startWith("Q"));

七. Optional类型

Optional<T>对象是一种包装器对象,要么包装了类型T的对象,要么没有包装任何对象

7.1 如何使用Optional值

有效的使用的关键是要使用这样的方法:它在值不存在的情况下会产生一个可替代物,而只有在值存在的情况下才会使用这个值。
第一条策略:
通常,在没有任何匹配时,我们会希望使用某个默认值,可能是空字符串
String result = optionalString.orElse(""); //有包装的字符串或如果是none就返回空字符串""
还可以调用代码来计算默认值:
String result = optionalString.orElseGet(()->Locale.getDefault().getDisplayName()); //这个方法仅仅在需要的时候调用
或者可以在没有任何值时抛出异常:

String result = optionalString.orElseThrow(IllegalStateException::new);
第二条策略:只有在其存在的情况下才消费该值。
ifPresent方法会接受一个函数。如果该可选值存在,那么它会被传递给该函数,否则不会发生任何事情。
optionalValue.ifPresent(v -> Process v);
eg:如果在该值存在的情况下想要将其添加到某个集中,那么就可以调用
optionalValue.ifPresent(v -> results.add(v));

或者

optionalValue.ifPresent(results::add);
当调用ifPresent时,从该函数不会返回任何值。如果想要处理函数的结果,应该使用map
Optional<Boolean> added = optionalValue.map(result::add);//现在的额added有三种值之一:在optionalValue存在的情况下包装在Optional中的true或false,以及在optionalValue不存在的情况下的空Optional

7.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();
get方法和isPresent方法不比原本的得到某物或null的方式更好
get会在Optional为空时,抛出NoSuchElementException对象。
isPresent如果该Optional不为空,则返回true。

7.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值和可选值之间的桥梁,Optional.ofNullable(obj)会在obj不为null的情况下返回Optional.of(obj),否则会返回Optional.empty()

7.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);//注意这里是optional后的flatMap和前面的stream.flatMap不同
如果s.f()的值存在,那么g就可以应用到它上面。否则返回一个空Optional<U>。如果有更多的可以产生Optional值的方法或Lambda表达式,那么就可以重复此过程。你可以直接将对flatMap的调用链接起来,从而构建由这些步骤构成的管道,只有所有步骤都成功时,该管道才会成功
public static Optional<Double> squareRoot(Double x){

return x<0?Optional.empty():Optional.of(Math.sqrt(x));

}

public static Optional<Double> inverse(Double x){

return x<0?Optional.empty():Optional.of(1/x);

}
之前有安全的inverse方法,然后下面计算倒数的平方根了:
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(),整个结果都会是空。

八. 收集结果

处理完流之后,通过会想要查看其元素。此时可以调用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 averageWordLength = summary.getAverage();//获取元素单词总的平均值
double maxWordLength = summary.getMax();//获取元素最大的长度

九. 收集到映射表中

假设我们有一个Stream<Person>,并且想要将其元素收集到一个映射表中,这样后续就可以通过它们的id来查找人员。Collectors.toMap方法有两个函数引元,它们用来产生映射表的键和值。例如
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()));//Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。
如果多个元素具有相同的键,那么就会存在冲突,收集器将会抛出一个IllegalStateException对象。可以通过提供第3个函数引元来覆盖这种行为,该函数会针对给定的已有值和新值来解决冲突并确定键对应的值。这个函数应该返回已有值、新值或它们的组合。
Stream<Local> locals = Stream.of(Local.getAvailableLocales());

Map<String,String> languageNames = locals.collect(

  Collectors.toMap(

    Local::getDisplayLanguage,

     l -> l.getDisplayLanguage(l),

    (existingValue, newValue) -> existingValue));

已有集和新集做并操作:

Map<String,set<String>> countryLanguageSets= locals.collect(

    Collectors.toMap(

     Local::getDisplayCountry,

     l -> Collections.singleton(l.getDispalyLanguage(l)),

     (a, b) -> {

        Set<String> union = new HashSet<>(a);

         union.addAll(b);

          return union;

}));
想得到TreeMap,需要将构造器作为第4个引元来提供。
Map<String,String> idToPerson= locals.collect(

Collectors.toMap(

Person::getId,

Function.identity(),

(existingValue, newValue) -> {throw new IllegalStateExcetion();}),

TreeMap::new

));//类似于这种Map iii = new TreeMap(); treemap是具体的实现类
toMap对应的另一个产生并发映射表的是toConcurrentMap

十. 群组和分区

将具有相同特性的值群聚成组是非常常见的,并且groupingBy方法直接就支持它。
Map<String, List<Locale>> countryToLocales = locales.collect(

Collectors.groupingBy(Locale::getCountry));
当分类函数是断言函数(即返回boolean值的函数)时,流的元素可以分区为两个列表:该函数返回true的元素和其他的元素。在这种情况下,使用partitioningBy比使用groupingBy要更高效。
Map<String, List<Locale>> countryToLocales = locales.collect(

Collectors.partitioningBy(l -> l.getLanguage().equal("en")));
并行的有groupingByConcurrent并发映射表,如果调用groupingByConcurrent方法,就会在使用并行流时获得一个被并行组装的并行映射表。这与toConturrentMap方法完全类似。

十一. 下游收集器

groupingBy方法会产生一个映射表,它的每个值都是一个列表。如果想要以某种方法来处理这些列表,就需要提供一个“下游收集器”。
eg: 如果想要获得集而不是列表,那么可以使用上一节中看到的Collector.toSet收集器:
Map<String, Set<Locale>> countryToLocaleSet = locales.collect(groupingBy(Locale::getCountry , toSet()));
java提供了多种可以将群组元素约简为数字的收集器
(1) .产生收集到的元素的个数:
Map<String, Long> countryToLocaleCounts = locales.collect(groupingBy(Locale::getCountry , counting()));
(2).summing(Int|Long|Double)会接收一个函数作为引元,将该函数应用到下游元素中,并产生他们的和
Map<String, Integer> stateToCityPopulation = cities.collect(groupingBy(Locale::getState, summingInt(City::getPopulation))); //计算城市流中每个州的人口总和
(3).minBy和maxBy会接受一个比较器,并产生下游元素中的最大值和最小值
Map<String, Option<City>> stateToLargestCity = cities.collect(groupingBy(Locale::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))))

);
以上是按照州将城市群组在一起。在每个州内部,生成了各个城市的名字,并按照最大长度约简
针对上一节的问题,即把某国所有语言收集到一个集中,产生了一种更佳的解决方案。
Map<String, Set<String>> countryToLanguages = locales.collect(

   groupingBy(Locale::getDisplayCountry,

      mapping(Locale::getDisplayLanguage, toSet()))

);
如果群组和映射函数的返回值为int、long或double,那么可以将元素收集到汇总统计对象中
Map<String, IntSummaryStatics> stateToCityPopulationSummary = cities.collect(

groupingBy(City::getState, summarizingInt(City::getPopulation))

);

十二.约简操作

reduce方法是一种用于从流中计算某个值的通用机制,其最简单的形式将接受一个二元函数,并从前两个元素开始持续应用它。如果该函数是求和函数那么就很容易解释这种机制
List<Integer> value=...;

Optional<Integer> sum = values.stream().reduce((x,y)->x+y); 这种情况也可以写成reduce(Integer::sum)
通常会有一个幺元值e使得e op x = x, 可以使用这个元素作为计算的起点。例如0是加法的幺元值,然后可以调用第二种形式的reduce:
Optional<Integer> sum = values.stream().reduce(0,(x,y)->x+y); //0+v1+v2+... 如果流为空则会返回幺元值。
现在假设你又一个对象流,并且想要对某些属性求和,例如字符串中所有字符串的长度,那么就不能使用简单形式的reduce,而是需要(T,T)-> T这样的函数,即引元和结果的类型相同的函数。但是这种情况下,你有两种类型:流的元素具有String类型,而累积结果是整数。有一种形式的reduce可以处理这种情况。
首先,需要提供一种“累积器”函数(total,word)->total+word.length(),这个函数会被反复调用,产生累积的总和。但是,当计算被并行化时,会有多个这种类型的计算,你需要将它们的结果合并,因此需要提供第二个函数来执行此处理,完整调用如下:
int result = words.reduce(0,

(total,word)->total+word.length(),

  (total1,total2)->total1+total2

)

十三. 基本类型流

将每个整数都包装到包装器对象中是很低效的。对其他基本类型来说情况也一样。提供了IntStream\LongStream\DoubleStream用来直接存储基本类型值,而无需使用包装器。
为了创建IntStream,需要调用IntStream.of和Arrays.stream方法:
IntStream stream = IntStream.of(1,2,3);

stream = Arrays.stream(values, from, to);//values是一个int[] array
与对象流一样,我们还可以使用静态的generate和iterate方法。此外,IntStream和LongStream有静态方法range和rangeClosed,可以生成步长为1的整数范围。
IntStream stream = IntStream.range(0,100);//上边界被排除

IntStream stream = IntStream.rangeClosed(0,100);//上边界被包含
CharSequence接口拥有codePoints和chars方法,可以生成由字符的Unicode码或由UTF-16编码机制的码元构成的IntStream.
String sequence = "\udddd\uddd is the set of octions.";

IntStream codes = setence.codePoints();
当你有一个对象流,可以用mapToInt,mapToLong和mapToDouble将其转换为基本类型流。
String<String> words = ... ;

IntStream lengths = words.mapToInt(String::length);
基本类型流转换为对象流,需要使用boxed方法。
Stream<Integer> integers = IntStream.range(0,100).boxed();
通常,基本类型流觞的方法和对象流上的方法类似。下面是最主要的差异:
(1) toArray方法会返回基本类型数据
(2) 产生可选结果的方法会返回一个OptionalInt、OptionalLong或OptionalDouble。这些类与Optional类类似,但是具有getAsInt\getAsLong和getAsDouble方法,而不是get方法。
(3)具有返回总和、平均值、最大值和最小值的sum\average\max和min方法。对象流没有定义这些方法
(4)summaryStatics方法会产生一个类型为IntSummaryStatics、LongSummaryStatics、DoubleSummaryStatics的对象,它们可以同时报告流的总和
注意:Random类具有ints\longs和doubles方法,它们会返回由随机数构成的基本类型流
更多看api

十四. 并行流

流使得并行处理块操作变得很容易。这个过程几乎是自动的,但是需要遵守一些规则,首先,必须有一个并行流。可以用Collection.parallelStream()方法从任何集合中获取一个并行流:
Stream<String> parallelwords = words.parallelStream();
而且parallel方法可以将任意的顺序流转换为并行流。
Stream<String> parallelwords = Stream.of(wordArray).parallel();
只要在终结方法执行时,流处于并行模式,那么所有的中间流操作都将被并行化。当流操作并行运行时,其目标是要让其返回结果与顺序执行时返回的结果相同。重要的是,这些操作可以以任意顺序执行。
如果传递给某个函数会在多个并发线程中运行,每个都会更新共享的数组,这是一种经典的竞争情况,如果多次运行这种程序,那么每次运行都会产生不同的值,每次都是错的。
你的职责是要确保传递给并行流操作的任何函数都可以安全的并行执行,达到这个目的的最佳方式是原理易变状态。
当放弃排序需求时,有些操作可以被更有效地并行化。通过在流觞调用unordered方法,就可以明确表示我们对排序不感兴趣。
还可以通过放弃排序要求来提高limit方法的速度。如果只想从流中取出任意n个元素,而并不在意到底要获取哪些,那么可以调用:
Stream<String> sample = words.parallelStream().unordered().limit(n);
如1.9节所讨论的,合并映射表的大家很高昂。正因为这个原因,Collectors.groupByConcurrent方法使用了共享的并发映射表。为了从并行化中获益,映射表中的值的顺序不会与流中的顺序相同。
Map<Integer, List<String>> result = words.parallelStream().collect(Collectors.groupingByConcurrent(String::length));
当然如果使用独立于排序的下游收集器,那么就不必在意了,例如:
Map<Integer, Long> wordCounts = words.parallelStream().collect(Collectors.groupingByConcurrent(String::length, counting()));
为了让并行流正常工作,需要满足大量的条件:
(1)数据应该在内存中。必须等到数据到达是非常低效的。
(2)流应该可以被高效地分成若干个子部份。由数组或平衡二叉树支撑的流都可以工作得很好,但是Stream.iterate返回的结果不行。
(3)流操作的工作量应该具有较大的规模。如果总工作负载并不是很大,那么搭建并行计算时所付出的代价就没有什么意义。
(4)流操作不应该被阻塞。
换句话说,不要将所有的流都转换为并行流。只有在对已经位于内存中的数据执行大量的计算操作时,才应该使用并行流。

0