本节内容是StreamAPI中常用操作的学习和理解,下面会专门再有一篇文章介绍在项目开发中那些高频使用的利用Stream处理对象集合的使用示例。 本文大纲如下: Java的StreamAPI提供了一种处理对象集合的函数式方法。Stream是Lambda表达式等其他几个函数式编程特性一起在Java8被引入的这个篇教程将解释这些函数式Stream是如何工作的,以及怎么使用它们。 注意,Java的StreamAPI与JavaIO的InputStream和OutputStream没有任何关系,不要因为名字类似造成误解。InputStream和OutputStream是与字节流有关,而Java的StreamAPI用于处理对象流。Stream的定义 Java的Stream是一个能够对其元素进行内部迭代的组件,这意味着它可以自己迭代其元素。相反地,当我们使用Collection的迭代功能,例如,从Collection获取Iterator或者使用Iterable接口的forEach方法这些方式进行迭代时,我们必须自己实现集合元素的迭代逻辑。 当然集合也支持获取Stream完成迭代,这些我们在介绍集合框架的相关章节都介绍过。流处理 我们可以将Listener方法或者叫处理器方法附加到Stream上。当Stream在内部迭代元素时,将以元素为参数调用这些处理器。Stream会为流中的每个元素调用一次处理器。所以每个处理器方法都可以处理Stream中的每个元素,我们把这称为流处理。 流的多个处理器方法可以形成一个调用链。链上的前一个处理器处理流中的元素,返回的新元素会作为参数传给链中的下一个处理器处理。当然,处理器可以返回相同的元素或新元素,具体取决于处理器的目的和用途。怎么获取流 有很多方法获取Stream,一般最常见的是从Collection对象中获取Stream。下面是一个从List对象获取Stream的例子。ListStringitemsnewArrayListString();items。add(one);items。add(two);items。add(three);StreamStringstreamitems。stream();复制代码 集合对象都实现了Collection接口,所以通过接口里定义的stream方法获救获取到由集合元素构成的Steam。流处理的构成 在对流进行处理时,不同的流操作以级联的方式形成处理链。一个流的处理链由一个源(source),0到多个中间操作(intermediateoperation)和一个终结操作(terminaloperation)完成。源:源代表Stream中元素的来源,比如我们上面看到的集合对象。中间操作:中间操作,在一个流上添加的处理器方法,他们的返回结果是一个新的流。这些操作是延迟执行的,在终结操作启动后才会开始执行。终结操作:终结流操作是启动元素内部迭代、调用所有处理器方法并最终返回结果的操作。 概念听起来有点模糊,我们通过流处理的例子再理解一下。importjava。util。ArrayLimportjava。util。Limportjava。util。stream。SpublicclassStreamExamples{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayList();stringList。add(ONE);stringList。add(TWO);stringList。add(THREE);StreamStringstreamstringList。stream();longcountstream。map((value)value。toLowerCase())。count();System。out。println(countcount);}}复制代码 map()方法的调用是一个中间操作。它只是在流上设置一个Lambda表达式,将每个元素转换为小写形式。而对count()方法的调用是一个终结操作。此调用会在内部启动迭代,开始流处理,这将导致每个元素都转换为小写然后计数。 将元素转换为小写实际上并不影响元素的计数。转换部分只是作为map()是一个中间操作的示例。流的中间操作 StreamAPI的中间(非终结)流操作是转换或者过滤流中元素的操作。当我们把中间操作添加到流上时,我们会得到一个新的流作为结果。下面是一个添加到流上的中间操作的示例,它的执行结果会产生一个新的流。ListStringstringListnewArrayList();stringList。add(ONE);stringList。add(TWO);stringList。add(THREE);StreamStringstreamstringList。stream();StreamStringstringStreamstream。map((value)value。toLowerCase());复制代码 上面例子中,流上添加的map()调用,此调用实际上返回一个新的Stream实例,该实例表示原始字符串流应用了map操作后的新流。只能将单个操作添加到给定的Stream实例上。如果需要将多个操作连接在一起,则只能将第二个操作应用于第一个操作产生的Stream实例上。StreamStringstringStream1stream。map((value)value。toLowerCase());StreamStringstringStream2stringStream1。map((value)value。toUpperCase());复制代码 注意第二个map()调用是如何在第一个map()调用返回的Stream上进行调用的。 我们一般是将Stream上的所有中间操作串联成一个调用链:StreamStringstream1stream。map((value)value。toLowerCase())。map((value)value。toUpperCase())。map((value)value。substring(0,3));复制代码 以map方法为代表流间操作方法的参数,是一个函数式接口,我们可以直接用Lambda表达式作为这些操作的参数。所以在介绍Lambda的那一节我们也说过,Lambda一般是和流操作就结合起来用的。 参考Java的函数式接口:tutorials。jenkov。comjavafuncti 下面我们说一下常用的流的中间操作。map map()方法将一个元素转换(或者叫映射)到另一个对象。例如,一个字符串列表,map()可以将每个字符串转换为小写、大写或原始字符串的子字符串,或完全不同的东西。ListStringlistnewArrayListString();StreamStringstreamlist。stream();StreamStringstreamMappedstream。map((value)value。toUpperCase());复制代码filter filter()用于从Stream中过滤掉元素。filter方法接受一个Predicate(也是一个函数式接口),filter()为流中的每个元素调用Predicate。如果元素要包含在filter()返回结果的流中,则Predicate应返回true。如果不应包含该元素,则Predicate应返回false。StreamStringlongStringsStreamstream。filter((value){元素长度大于等于3,返回true,会被保留在filter产生的新流中。returnvalue。length()3;});复制代码 比如Stream实例应用了上面这个filter后,filter返回的结果流里只会包含长度不小于3的元素。flatMap flatMap方法接受一个Lambda表达式,Lambda的返回值必须也是一个stream类型,flatMap方法最终会把所有返回的stream合并。map与flatMap方法很像,都是以某种方式转换流中的元素。如果需要将每个元素转换为一个值,则使用map方法,如果需要将每个元素转换为多个值组成的流,且最终把所有元素的流合并成一个流,则需要使用flatMap方法。在效果上看是把原来流中的每个元素进行了展平importjava。util。ArrayLimportjava。util。Aimportjava。util。Limportjava。util。stream。SpublicclassStreamFlatMapExamples{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayListString();stringList。add(Oneflewoverthecuckoosnest);stringList。add(Tokillamuckingbird);stringList。add(Gonewiththewind);StreamStringstreamstringList。stream();stream。flatMap((value){String〔〕splitvalue。split();returnArrays。asList(split)。stream();})。forEach((value)System。out。println(value));}}复制代码 在上面的例子中,每个字符串元素被拆分成单词,变成一个List,然后从这个List中获取并返回流,flatMap方法最终会把这些流合并成一个,所以最后用流终结操作forEach方法,遍历并输出了每个单词。OneflewoverthecuckoosnestTokillamuckingbirdGonewiththewind复制代码distinct distinct()会返回一个仅包含原始流中不同元素的新Stream实例,任何重复的元素都将会被去掉。ListStringstringListnewArrayListString();stringList。add(one);stringList。add(two);stringList。add(three);stringList。add(one);StreamStringstreamstringList。stream();ListStringdistinctStringsstream。distinct()。collect(Collectors。toList());System。out。println(distinctStrings);复制代码 在这个例子中,元素one在一开始的流中出现了两次,原始流应用distinct操作生成的新流中将会丢弃掉重复的元素,只保留一个one元素。所以这个例子最后的输出是:〔one,two,three〕复制代码limit limit操作会截断原始流,返回最多只包含给定数量个元素的新流。importjava。util。ArrayLimportjava。util。Limportjava。util。stream。SpublicclassStreamLimitExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayListString();stringList。add(one);stringList。add(two);stringList。add(three);stringList。add(one);StreamStringstreamstringList。stream();stream。limit(2)。forEach(elementSystem。out。println(element));}}复制代码 这个例子中,因为对原始流使用了limit(2)操作,所以只会返回包含两个元素的新流,随后使用forEach操作将它们打印了出来。程序最终将会输出:onetwo复制代码peek peek()方法是一个以Consumer(java。util。function。Consumer,Consumer代表的是消费元素但不返回任何值的方法)作为参数的中间操作,它返回的流与原始流相同。当原始流中的元素开始迭代时,会调用peek方法中指定的Consumer实现对元素进行处理。 正如peek操作名称的含义一样,peek()方法的目的是查看流中的元素,而不是转换它们。跟其他中间操作的方法一样,peek()方法不会启动流中元素的内部迭代,流需要一个终结操作才能开始内部元素的迭代。 peek()方法在流处理的DEBUG上的应用甚广,比如我们可以利用peek()方法输出流的中间值,方便我们的调试。Stream。of(one,two,three,four)。filter(ee。length()3)。peek(eSystem。out。println(Filteredvalue:e))。map(String::toUpperCase)。peek(eSystem。out。println(Mappedvalue:e))。collect(Collectors。toList());复制代码 上面的例子会输出以下调试信息。Filteredvalue:threeMappedvalue:THREEFilteredvalue:fourMappedvalue:FOUR复制代码流的终结操作 Stream的终结操作通常会返回单个值,一旦一个Stream实例上的终结操作被调用,流内部元素的迭代以及流处理调用链上的中间操作就会开始执行,当迭代结束后,终结操作的返回值将作为整个流处理的返回值被返回。longcountstream。map((value)value。toLowerCase())。map((value)value。toUpperCase())。map((value)value。substring(0,3))。count();复制代码 Stream的终结操作count()被调用后整个流处理开始执行,最后将count()的返回值作为结果返回,结束流操作的执行。这也是为什么把他们命名成流的终结操作的原因。 上面例子,应用的中间操作map对流处理的结果并没有影响,这里只是做一下演示。 下面我们把常用的流终结操作说一下。anyMatch anyMatch()方法以一个Predicate(java。util。function。Predicate接口,它代表一个接收单个参数并返回参数是否匹配的函数)作为参数,启动Stream的内部迭代,并将Predicate参数应用于每个元素。如果Predicate对任何元素返回了true(表示满足匹配),则anyMatch()方法的结果返回true。如果没有元素匹配Predicate,anyMatch()将返回false。importjava。util。ArrayLimportjava。util。Limportjava。util。stream。SpublicclassStreamAnyMatchExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayListString();stringList。add(Oneflewoverthecuckoosnest);stringList。add(Tokillamuckingbird);stringList。add(Gonewiththewind);StreamStringstreamstringList。stream();booleananyMatchstream。anyMatch((value)value。startsWith(One));System。out。println(anyMatch);}}复制代码 上面例程的运行结果是true,因为流中第一个元素就是以One开头的,满足anyMatch设置的条件。allMatch allMatch()方法同样以一个Predicate作为参数,启动Stream中元素的内部迭代,并将Predicate参数应用于每个元素。如果Predicate为Stream中的所有元素都返回true,则allMatch()的返回结果为true。如果不是所有元素都与Predicate匹配,则allMatch()方法返回false。importjava。util。ArrayLimportjava。util。Limportjava。util。stream。SpublicclassStreamAllMatchExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayListString();stringList。add(Oneflewoverthecuckoosnest);stringList。add(Tokillamuckingbird);stringList。add(Gonewiththewind);StreamStringstreamstringList。stream();booleanallMatchstream。allMatch((value)value。startsWith(One));System。out。println(allMatch);}}复制代码 上面的例程我们把流上用的anyMatch换成了allMatch,结果可想而知会返回false,因为并不是所有元素都是以One开头的。noneMatch Match系列里还有一个noneMatch方法,顾名思义,如果流中的所有元素都与作为noneMatch方法参数的Predicate不匹配,则方法会返回true,否则返回false。importjava。util。ArrayLimportjava。util。Limportjava。util。stream。SpublicclassStreamNoneExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayList();stringList。add(abc);stringList。add(def);StreamStringstreamstringList。stream();booleannoneMatchstream。noneMatch((element){returnxyz。equals(element);});System。out。println(noneMatchnoneMatch);输出noneMatchtrue}}复制代码collect collect()方法被调用后,会启动元素的内部迭代,并将流中的元素收集到集合或对象中。importjava。util。ArrayLimportjava。util。Limportjava。util。stream。Cimportjava。util。stream。SpublicclassStreamCollectExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayList();stringList。add(Oneflewoverthecuckoosnest);stringList。add(Tokillamuckingbird);stringList。add(Gonewiththewind);StreamStringstreamstringList。stream();ListStringstringsAsUppercaseListstream。map(valuevalue。toUpperCase())。collect(Collectors。toList());System。out。println(stringsAsUppercaseList);}}复制代码 collect()方法将收集器Collector(java。util。stream。Collector)作为参数。在上面的示例中,使用的是Collectors。toList()返回的Collector实现。这个收集器把流中的所有元素收集到一个List中去。count count()方法调用后,会启动Stream中元素的迭代,并对元素进行计数。importjava。util。ArrayLimportjava。util。Aimportjava。util。Limportjava。util。stream。SpublicclassStreamExamples{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayListString();stringList。add(Oneflewoverthecuckoosnest);stringList。add(Tokillamuckingbird);stringList。add(Gonewiththewind);StreamStringstreamstringList。stream();longcountstream。flatMap((value){String〔〕splitvalue。split();returnArrays。asList(split)。stream();})。count();System。out。println(countcount);count14}}复制代码 上面的例程中,首先创建一个字符串List,然后获取该List的Stream,为其添加了flatMap()和count()操作。count()方法调用后,流处理将开始迭代Stream中的元素,处理过程中字符串元素在flatMap()操作中被拆分为单词、合并成一个由单词组成的Stream,然后在count()中进行计数。所以最终打印出的结果是count14。findAny findAny()方法可以从Stream中找到单个元素。找到的元素可以来自Stream中的任何位置。且它不提供从流中的哪个位置获取元素的保证。importjava。util。ArrayLimportjava。util。Limportjava。util。Oimportjava。util。stream。SpublicclassStreamFindAnyExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayList();stringList。add(one);stringList。add(two);stringList。add(three);stringList。add(one);StreamStringstreamstringList。stream();OptionalStringanyElementstream。findAny();if(anyElement。isPresent()){System。out。println(anyElement。get());}else{System。out。println(notfound);}}}复制代码 findAny()方法会返回一个Optional,意味着Stream可能为空,因此没有返回任何元素。我们可以通过Optional的isPresent()方法检查是否找到了元素。 Optional类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回容器中的对象,否则抛出异常:NoSuchElementExceptionfindFirst findFirst()方法将查找Stream中的第一个元素,跟findAny()方法一样,也是返回一个Optional,我们可以通过Optional的isPresent()方法检查是否找到了元素。importjava。util。ArrayLimportjava。util。Limportjava。util。Oimportjava。util。stream。SpublicclassStreamFindFirstExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayList();stringList。add(one);stringList。add(two);stringList。add(three);stringList。add(one);StreamStringstreamstringList。stream();OptionalStringanyElementstream。findFirst();if(anyElement。isPresent()){System。out。println(anyElement。get());}else{System。out。println(notfound);}}}复制代码forEach forEach()方法我们在介绍Collection的迭代时介绍过,当时主要是拿它来迭代List的元素。它会启动Stream中元素的内部迭代,并将Consumer(java。util。function。Consumer,一个函数式接口,上面介绍过)应用于Stream中的每个元素。注意forEach()方法的返回值是void。importjava。util。ArrayLimportjava。util。Limportjava。util。stream。SpublicclassStreamExamples{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayListString();stringList。add(one);stringList。add(two);stringList。add(three);StreamStringstreamstringList。stream();stream。forEach(System。out::println);}}复制代码 注意,上面例程中forEach的参数我们直接用了Lambda表达式引用方法的简写形式。min min()方法返回Stream中的最小元素。哪个元素最小是由传递给min()方法的Comparator接口实现来确定的。importjava。util。ArrayLimportjava。util。Limportjava。util。Oimportjava。util。stream。SpublicclassStreamMinExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayList();stringList。add(abc);stringList。add(def);StreamStringstreamstringList。stream();作为min方法参数的Lambda表达式可以简写成String::compareToOptionalStringminstream。min(String::compareTo);OptionalStringminstream。min((val1,val2){returnval1。compareTo(val2);});StringminStringmin。get();System。out。println(minString);abc}}复制代码 min()方法返回的是一个Optional,也就是它可能不包含结果。如果为空,直接调用Optional的get()方法将抛出异常NoSuchElementException。比如我们把上面的List添加元素的两行代码注释掉后,运行程序就会报Exceptioninthreadmainjava。util。NoSuchElementException:Novaluepresentatjava。util。Optional。get(Optional。java:135)atcom。example。StreamMinExample。main(StreamMinExample。java:21)复制代码 所以最好先用Optional的ifPresent()判断一下是否包含结果,再调用get()获取结果。max 与min()方法相对应,max()方法会返回Stream中的最大元素,max()方法的参数和返回值跟min()方法的也都一样,这里就不再过多阐述了,只需要把上面求最小值的方法替换成求最大值的方法max()即可。OptionalStringminstream。max(String::compareTo);复制代码reduce reduce()方法,是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。reduce()方法接收一个函数式接口BinaryOperator的实现,它定义的一个apply()方法,负责把上次累加的结果和本次的元素进行运算,并返回累加的结果。importjava。util。ArrayLimportjava。util。Limportjava。util。Oimportjava。util。stream。SpublicclassStreamReduceExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayList();stringList。add(Oneflewoverthecuckoosnest);stringList。add(Tokillamuckingbird);stringList。add(Gonewiththewind);StreamStringstreamstringList。stream();OptionalStringreducedstream。reduce((value,combinedValue)combinedValuevalue);写程序的时候记得别忘了reduced。ifPresent()检查结果里是否有值System。out。println(reduced。get());}}复制代码 reduce()方法的返回值同样是一个Optional类的对象,所以在获取值前别忘了使用ifPresent()进行检查。 streadm实现了多个版本的reduce()方法,还有可以直接返回元素类型的版本,比如使用reduce实现整型Stream的元素的求和importjava。util。ArrayLimportjava。util。LpublicclassIntegerStreamReduceSum{publicstaticvoidmain(String〔〕args){ListIntegerintListnewArrayList();intList。add(10);intList。add(9);intList。add(8);intList。add(7);IntegersumintList。stream()。reduce(0,Integer::sum);System。out。printf(List求和,总和为s,sum);}}复制代码 toArray toArray()方法是一个流的终结操作,它会启动流中元素的内部迭代,并返回一个包含所有元素的Object数组。ListStringstringListnewArrayListString();stringList。add(Oneflewoverthecuckoosnest);stringList。add(Tokillamuckingbird);stringList。add(Gonewiththewind);StreamStringstreamstringList。stream();Object〔〕objectsstream。toArray();复制代码 不过toArray还有一个重载方法,允许传入指定类型数组的构造方法,比如我们用toArray把流中的元素收集到字符串数组中,可以这么写:String〔〕strArraystream。toArray(String〔〕::new);复制代码流的拼接 Java的Stream接口包含一个名为concat()的静态方法,它可以将两个流连接成一个。importjava。util。ArrayLimportjava。util。Limportjava。util。stream。Cimportjava。util。stream。SpublicclassStreamConcatExample{publicstaticvoidmain(String〔〕args){ListStringstringListnewArrayList();stringList。add(Oneflewoverthecuckoosnest);stringList。add(Tokillamuckingbird);stringList。add(Gonewiththewind);StreamStringstream1stringList。stream();ListStringstringList2newArrayList();stringList2。add(LordoftheRings);stringList2。add(PlanetoftheRats);stringList2。add(PhantomMenace);StreamStringstream2stringList2。stream();StreamStringconcatStreamStream。concat(stream1,stream2);ListStringstringsAsUppercaseListconcatStream。collect(Collectors。toList());System。out。println(stringsAsUppercaseList);}}复制代码从数组创建流 上面关于Stream的例子我们都是从Collection实例的stream()方法获取的集合包含的所有元素的流,除了这种方法之外,Java的Stream接口中提供了一个名为of的静态方法,能支持从单个,多个对象或者数组对象快速创建流。importjava。util。stream。SpublicclassStreamExamples{publicstaticvoidmain(String〔〕args){StreamStringstream1Stream。of(one,two,three);StreamStringstream2Stream。of(newString〔〕{one,two});System。out。println(stream1。count());输出3System。out。println(stream2。count());输出2}}复制代码总结 上面我们把Stream的两大类操作:流的中间操作、流的终结操作都有哪些方法给大家列举了一遍,让大家对Stream能完成的操作有了大致的印象。不过为了讲解这些操作用的都是非常简单的例子,流操作的数据也都是简单类型的,主要的目的是让大家能更快速地理解Stream的各种操作应用在数据上后,都有什么效果。 作者:kevinyan 链接:https:juejin。cnpost7156055319490232327