赞
踩
"流"的概念:流是数据的渠道。因此,流代表了一个对象序列。流操作数据源,如数组或集合。流本身不存储数据,而只是移动数据,在移动过程中可能会对数据执行过滤、排序或其他操作。然而,一般来说,流操作本身不会修改数据源。例如,对流排序不会修改数据源的顺序。相反,对流排序会创建一个新流,其中包含排序后的结果。
注意
这里使用的"流"与I/O类使用的"流"不同。虽然I/O类的行为在概念上与java.util.stream中定义的流类似,但它们是不同的。因此,本篇中使用"流"这个属于时,指的是这里描述的某个流类型的对象。
流API定义了几个流接口,包含在java.util.stream中,BaseStream是基础接口,它定义了所有流都可以使用的基本功能。它是一个泛型接口,其声明如下所示:
interface BaseStream<T,s extends BaseStream<T,S>>
其中,T指定流中元素的类型,S指定扩展了BaseStream的流的类型。BaseStream扩展了AutoCloseable接口,所以可以使用带资源的try语句管理流。但是,一般来说,只有当流使用的数据源需要关闭时(如流连接到文件),才需要关闭流。大多数时候,例如数据源是结合的情况,不需要关闭流。表1列出了BaseStream接口声明的方法。
方 法 | 描 述 |
---|---|
void close() | 调用注册的关闭处理程序,关闭调用流(如前所述,很少有流需要被关闭) |
boolean isParallel | 如果流是并行流,返回true;如果流是顺序流,返回false |
Iterator<T> iterator() | 获得流的一个迭代器,并返回对该迭代器的引用(中端操作) |
S onClose(Runnable handler) | 返回一个新流,handler指定了该流的关闭处理程序。当关闭流时,将调用这个处理程序(中间操作) |
S parallel() | 基于调用流,返回一个并行流。如果调用流已经是并行流,就返回该流(中间操作) |
S sequential() | 基于调用流,返回一个顺序流。如果调用流已经是顺序流,并返回该流(中间操作) |
Spliterator<T> spliterator() | 获得流的spliterator,并返回其引用(终端操作) |
S unordered() | 基于调用流,返回一个无序流。如果调用流已经是无序流,就返回该流(中间操作) |
BaseStream接口派生了几个流接口,其中最具一般性的是Stream接口,其声明如下所示:
interface Stream<T>
其中,T指定流中元素的类型。因为Stream是泛型接口,所以可用于所有索引类型。除了继承自BaseStream的方法,Stream接口还定义了几个自己的方法,表2列出了其中的几个。
方 法 | 描 述 |
---|---|
<R,A> R collect(Collector<? super T,A,R collectorFunc>) | 将元素收集到一个可以修改的容器中,并返回该容器,这被称为可变缩减操作。R指定结果容器的类型,T指定调用流的元素类型,A指定内部累加的类型,collectorFunc指定收集过程的工作方式(终端操作) |
long count() | 统计流中的元素数,并返回结果(终端操作) |
Stream<T> filter(Predicate<? super T> pred) | 产生一个流,其中包含调用流中满足pred指定的谓词的元素(中间操作) |
void forEach(Consumer<? super T> action) | 对于调用流中的每个匀速,执行由action指定的动作(终端操作) |
<R> Stream<R> map(Function<? super T,? extends R> mapFunc) | 对调用流中的元素应用mapFunc,产生包含这些元素的一个新的DoubleStream流(中间操作) |
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapFunc) | 对调用流中的元素应用mapFunc,产生包含这些元素的一个新的DoubleStream流(中间操作) |
IntStream mapToInt(ToIntFunction<? super T> mapFunc) | 对流中的元素应用mapFunc,产生包含这些元素的一个新的LongStream流(中间操作) |
LongStream mapToLong(ToLongFunction<? super T> mapFunc) | 对调用流中的元素应用mapFunc,产生包含这些元素的一个新的LongStream流(中间操作) |
Optional<T> max(Comparator<? super T> comp) | 使用由comp指定的排序,找出并返回调用 流中的最大元素(终端操作) |
Optional<T> min(Comparator<? super T> comp) | 使用由comp指定的排序,找出并返回调用 流中的最小元素(终端操作) |
T reduce(T identityVal,BinaryOperator<T> accumulator) | 基于调用流中的元素返回结果,这被叫做缩减操作(终端操作) |
T reduce(T identityVal,BinaryOperator<T> accumulator) | 基于调用流中的元素返回结果,这被叫做缩减操作(终端操作) |
Stream<T> sorted() | 产生一个新流,其中包含按自然顺序的调用流的元素(中间操作) |
Object[] toArray() | 使用调用流的元素创建数组(终端操作) |
在表1和表2中,注意许多方法都被标注为"终端操作"或"中间操作"。二者的区别非常重要。终端操作为消费流。这种操作用于产生结果,例如找出流中最小的值,或者执行某种操作,比如forEach()方法。一个流被消费以后,就不能被重用。中间操作会产生另一个流。因此,中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生。这种机制被称为延迟行为,所以称中间操作行为延迟发生的。延迟行为让流API能够更加高效地执行。
流的另外一个关键点是,一些中间操作是无状态的,另外一些事有状态的。在无状态操作中,独立于其他元素处理每个元素。在无状态操作中,某个元素的处理可能依赖于其他元素。例如,排序是有状态操作,因为元素的顺序依赖于其他元素的值。因此,sorted()方法是有状态的。然而,基于无状态为此的元素过滤是无状态的。当需要并行处理流时,无状态与有状态的区别尤为重要,因为有状态操作可能需要几次处理才能完成。
因为Stream操作的是对象引用,所以不能直接操作基本类型。为了处理基本类型流,流API定义了以下接口:
DoubleStream
IntStream
LongStream
这些流都扩展了BaseStream,并且具有类似于Stream的功能,只不过它们操作的是基本类型,而不是引用类型。它们也提供了便捷方法,例如boxed(),使得它们更加方便。因为对象流最为常见。本篇重点关注Stream,但是基本类型流的处理基本上是相同的。
获得流的方式有多种。可能最常见的是获得集合流。从JDK8开始,Collection接口已被扩展,包含了两个可以获得集合流的方法。第一个方法是stream(),如下所示:
default Stream<E> stream()
该方法的默认实现返回一个顺序流。第二个方法是parallelStream(),如下所示:
default Stream<E> parallelStream()
该方法的默认实现返回一个并行流(但是,如果无法获得并行流,也可能返回一个顺序流)。并行流支持流操作的并行执行。因为每个几个都实现了Collection接口,所以可以使用这两个方法从任意流类获得流,例如ArrayList或HashSet。
通过使用静态的stream()方法,也可以获得数组流。该方法是JDK8添加到Arrays类中的,它的一种形式如下所示:
static<T> Stream<T> stream(T[] array)
该方法返回array中元素的一个顺序流。例如,给定类型的Address的address数组,下面的代码将获得该数组的流:
Stream<Address> addrStrm = Arrays.stream(address);
stream()方法还有几个重载形式,例如有的形式能够处理基本类型的数组,它们返回流的类型为IntStream、DoubleStream或LongStream。
还有另外一些方法可以获得流。例如,许多流操作会返回新流,而且通过对BufferReader调用lines()方法,可以获得I/O源的流。不管流是如果获得的,使用方式都与其他流一样。
下面的程序创建了一个叫做myList的ArrayList,用于存储整数集合(这些整数被自动装箱为Integer引用类型)。然后,获得一个使用myList作为源的流。最后,程序演示了各种流操作。
//Demonstrate several stream operations. import java.util.ArrayList; import java.util.Optional; import java.util.stream.Stream; class StreamDemo { public static void main(String[] args) { //Create a list of Integer values. ArrayList<Integer> myList = new ArrayList<>(); myList.add(7); myList.add(18); myList.add(10); myList.add(24); myList.add(17); myList.add(5); System.out.println("Original list: "+myList); //Obtain a Stream to the array list. Stream<Integer> myStream = myList.stream(); //Obtain the minimum and maximum value by use of min(), //max(),isPresent(),and get(). Optional<Integer> minVal = myStream.min(Integer::compare); if(minVal.isPresent()) System.out.println("Minimum value: "+minVal.get()); //Must obtain a new stream because previous call to min() //is a terminal operation that consumed the stream. myStream = myList.stream(); Optional<Integer> maxVal = myStream.max(Integer::compare); if(maxVal.isPresent()) System.out.println("Maximum value: "+maxVal.get()); //Sort the stream by use of sorted(). Stream<Integer> sortedStream = myList.stream().sorted(); //Display the sorted stream by use of forEach(). System.out.print("Sorted stream: "); sortedStream.forEach((n)-> System.out.print(n+" ")); System.out.println(); //Display only the odd values by use of filter(). Stream<Integer> oddVals = myList.stream().sorted().filter((n)->(n%2)==1); System.out.print("Odd values: "); oddVals.forEach((n)-> System.out.print(n+" ")); System.out.println(); //Display only the odd values that are greater than 5.Notice that //two filter operations are pipelined. oddVals = myList.stream().sorted().filter((n)->(n%2)==1) .filter((n)->n>5); System.out.print("Odd values greator than 5: "); oddVals.forEach((n)-> System.out.print(n+" ")); /** * 输出: * Original list: [7, 18, 10, 24, 17, 5] * Minimum value: 5 * Maximum value: 24 * Sorted stream: 5 7 10 17 18 24 * Odd values: 5 7 17 * Odd values greator than 5: 7 17 */ } }
下面仔细看看每个流操作。创建ArrayList以后,程序通过调用stream()方法获得此列表的流,如下所示:
Stream<Integer> myStream = myList.stream();
如前所述,现在Collection接口定义了stream()方法,可以调用集合获得一个流。因为每个集合流都实现了Collection接口,所以可以使用stream()方法来获得任意类型的集合的流。包括这里使用的ArrayList集合。这是将对流的引用赋给了myStream。
接下来,程序获得了流中的最小值(当然,也是数据源中的最小值),并显示出该流,如下所示:
Optional<Integer> minVal = myStream.min(Integer::compare);
if(minVal.isPresent())System.out.println("Minimum value: "+minVal.get());
表2,min()方法的声明如下所示:
Optional<T> min(Comparator<? super T comp>)
首先注意,min()方法的参数类型是Comparator。该比较器用于比较流中的两个元素。本例中,将对Integer的compare()方法的引用传递给min()方法,compare()方法用于实现比较两个Integer的比较器。接下来,注意min()方法的返回类型是Optional。Optional是打包在java.util中的一个泛型类,其声明如下所示:
class Optional<T>
其中,T指定了元素类型。Optional接口可以包含T类型的值,也可以为空。使用isPresent()方法可以判断是否存在值。假设存在值,那么可以调用get()方法以获取该值。在示例中,返回的对象将以Integer对象的形式包含流中的最小值。
关于上述代码,还有一点需要注意:min()是消费流的终端操作。因此,在min()方法执行后,不能再使用myStream。
下面的代码获得并显示流中的最大值:
myStream = myList.stream();
Optional<Integer> maxVal = myStream.max(Integer::compare);
if(maxVal.isPresent())
System.out.println("Maximum value: "+maxVal.get())
首先,再次将myList.stream()方法返回的流赋给myStream。如刚才所述,这么做是有必要的,因为刚才及调用的min()方法消费了前一个流。所以,需要一个新的流。接下来,调用max()方法来获得最大值。与min()一样,max()返回一个Optional对象。其值时通过调用get()方法获得的。
然后,程序通过下面的代码行获得一个排序后的流:
Stream<Integer> sortedStream = myList.stream().sorted();
这里对myList.stream()返回的流调用sorted()方法。因为sorted()是中间操作,所以其结果是一个新流,也就是赋给sortedStream的流。排序流的内容通过使用forEach()方法显示了出来:
sortedStream.forEach(n)->System.out.print(n+" ");
其中,forEach()方法对流中的每个元素执行了操作。在本例中,它简单地为sortedStream中的每个元素调用了System.out.print()方法。这是通过使用一个lambda表达式完成的。forEach()方法的一般形式如下所示:
void forEach(Consumer<? super T> action)
Consumer是java.util.function包中声明的一个泛型函数式接口,其抽象方法为accept(),如下所示:
void accept(T objRef)
forEach()调用中使用的lambda表达式提供了accept()方法的实现。forEach()方法是终端操作。因此,在该方法执行后,流就被消费掉了。
接下来,使用filter()方法过滤排序后的流,使其只包含奇数值:
Stream<Integer> oddVals = myList.stream().sorted().filter((n)->(n%2)==1);
filter()方法基于一个谓词过滤流,它返回一个只包含满足谓词的元素的新流。该方法如下所示:
Stream<T> filter(Predicate<? super T> pred)
Predicate是java.util.function中定义的一个泛型函数式接口,其抽象方法为test(),如下所示:
boolean test(T objRef)
如果objRef引用的对象满足谓词,该方法返回true,否则返回false。传递给filter()方法的lambda表达式实现了这个方法。因为filter()是中间操作,所以返回一个包含过滤后的值的新流,在本例中,过滤后的值就是奇数值。然后,像前面一样,使用forEach()方法以显示这些元素。
因为filter()方法或其他任何中间操作会返回一个新流,所以可以对过滤后的流再次执行过滤操作。下面的代码演示了这一点,得到了一个只包含大于5的奇数的流:
oddVals = myList.stream().filter((n)->(n%2)==1).filter((n)->n>5);
考虑前面示例程序中的min()和max()方法。这两个都是终端方法,基于流中的元素返回结果。用流API的术语来说,它们代表了缩减操作,因为每个操作都将一个缩减为一个值——对于这两种操作,就是最小值和最大值。流API将这两种操作称为特例缩减,因为它们执行了具体的操作。除了min()和max(),还存在其他特例缩减操作,如统计流中元素个数的count()方法。然而,流API泛化了这种概念,提供了reduce()方法。通过使用reduce()方法,可以基于任意条件,从流中返回一个值。根据定义,所有缩减操作都是终端操作。
Stream定义了三个版本的reduce()方法。其中的两个版本如下所示:
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identityVal,BinaryOperator<T> accumulator)
第一个版本返回Optional类型的对象,该对象包含了结果。第二个版本返回T类型的对象(T类型是流中元素的类型)。在这两种形式中,accumulator是一个操作两个值并得到结束的函数。在第二种形式中,identityVal是这样一个值:对于涉及identityVal和流中任意元素的累积操作,得到的结果就是元素自身,没有改变。例如,如果操作是加法,identityVal是0,因为0+x是x。对于乘法操作,identityVal是1,因为1*x是x。
BinaryOperator是java.util.function包中声明的一个函数式接口,它扩展了BiFunction函数式接口。BiFunction定义了如下抽象方法:
R apply(T val,U val2)
其中,R指定了结果类型,T是第一个操作数的类型,U是第二个操作数的类型。因此,apply()对其两个操作数(val和val2)应用一个函数,并返回结果。BinaryOperator扩展BiFunction时,为所有类型参数指定了相同的类型。因此,对于BinaryOperator来说,apply()如下所示:
T apply(T val,T val2)
此外,在用到reduce()中时,val将包含前一个结果,val2将包含下一个元素。在第一次调用时,取决于所使用的reduce()版本,val将包含单位值或第一个元素。
需要理解的是,累加器必须满足一下三个约束:
(10*2)*7
得到的结果与下面的运算相同
10*(2*7)
下面的程序演示了刚才描述的reduce()版本:
//Demonstrate the reduce() method. import java.util.ArrayList; import java.util.Optional; class StreamDemo2 { public static void main(String[] args) { //Create a list of Integer values. ArrayList<Integer> myList = new ArrayList<>(); myList.add(7); myList.add(18); myList.add(10); myList.add(24); myList.add(17); myList.add(5); //Two ways to obtain the integer product of the elements //in myList by use of reduce(). Optional<Integer> productObj = myList.stream().reduce((a, b) -> a * b); if (productObj.isPresent()) System.out.println("Product as Optional: " + productObj.get()); int product = myList.stream().reduce(1, (a, b) -> a * b); System.out.println("Product as int: " + product); /** * 输出: * Product as Optional: 2570400 * Product as int: 2570400 */ } }
可以看出,reduce()方法的两次使用得到了相同的结果。
在程序中,第一个版本的reduce()方法使用lambda表达式来计算两个值的乘积。在本例中,因为流中包含Integer值,所以在乘法计算中会自动拆箱Integer对象,然后再返回结果时会自动重新装箱。两个值分别代表累积结果中的当前值和流中的下一个元素。最终结果放在一个Optional类型的对象中并被返回。通过对返回的对象调用get()方法,可以获得这个值。
在第二个版本中,显式指定了单位值,对于乘法而言就是1.注意,结果作为元素类型的对象返回,在本例中就是一个Integer对象。
虽然对于示例而言,简单的缩减操作很有用。如乘法操作,但是缩减操作不限于此。例如,对于前面的程序,下面的代码可以获得偶数值的乘积:
int eventProduct = myList.stream().reduce(1,(a,b)->{if(b%2==0) return a*b; else return a;})
借助多核处理器并执行代码可以显著提高性能。然而,并行编程可能十分复杂且容易出错。流库提供的好处之一是能够可靠地并行执行一些操作。
请求并行处理流:只需要一个并行流即可。如前所述,获得并行流的一种方式是使用Collection定义的parallelStream()方法。另一种方法是对顺序流调用parallel()方法。parallel()方法由BaseStream定义,如下所示:
S parallel()
该方法基于调用它的顺序流,返回一个并行流(如果调用该方法的流已经是一个并行流,就返回该调用流)。即使对于并行流,也只有在环境支持的情况下才可以实现并行处理。
获得并行流后,如果环境支持并行处理,那么在该流上发生的操作就可以并行执行。例如,在前面的程序中,如果把stream()调用替换为parallelStream(),第一个reduce()操作就可以并行进行:
Optional<Integer> productObj = myList.parallelStream().reduce((a,b)->a*b);
结果是一样的,但是乘法操作可以同时发生在不同的线程上。
一般来说,应用到并行流的任何操作都必须是无状态的。另外,还必须是不干预的,并且具有关联性。这确保在并行流上执行操作得到的结果,与在顺序流上执行相同操作得到的结果相同。
使用并行流时,可能会发现下面这个版本的reduce()方法十分有用。该版本可以指定如何合并部分结果:
<U> U reduce(U identityVal,BiFunction<U,? super T,U> accumulator,BinaryOperator<U> combiner)
在这个版本中,combiner定义的函数将accumulator函数得到的两个值合并起来。对于前面的程序,下面的语句通过使用并行流,计算出myList中元素的积:
int parallelProduct = myList.parallelStream().reduce(1,(a,b)->a*b,(a,b)->a*b);
可以看到,在这个例子中,accumulator和combiner执行的是相同的操作。但是,在有些情况下,accumulator的操作与combiner的操作必须不同。例如,分析下面的程序。这里,myList包含一个double值的列表。它使用reduce()方法的合并器版本,计算列表中每个元素我的平方根的积。
//Demonstrate the use of a combiner with reduce() import java.util.ArrayList; class StreamDemo3 { public static void main(String[] args) { //This is a now a list of double values. ArrayList<Double> myList = new ArrayList<>(); myList.add(7.0); myList.add(18.0); myList.add(10.0); myList.add(24.0); myList.add(17.0); myList.add(5.0); double productOfSqrRoots = myList.parallelStream().reduce( 1.0,(a,b)->a*Math.sqrt(b), (a,b)->a*b ); System.out.println("Product of square roots: "+productOfSqrRoots); } }
注意,累加器函数将两个元素的平方根相乘。因此,这两个函数是不同的。不止如此,对于这种计算,这两个函数必须相同,结果才会正确。例如,如果尝试使用下面的语句来获得元素的平方根的乘积,将会发生错误:
//This won't work.
double productOfSqrRoots2=myList.parallelStream().reduce(1.0,(a,b)->a*Math.sqrt(b));
在这个版本的reduce()方法中,累加器函数和合并函数式同一个函数。这将导致错误,因为当合并两个部分结果时,相乘的是它们的平方根,而不是部分结果自身。
值得注意的是,上面对reduce()方法的调用中,如果将流改为顺序流,那么操作将得到正确的结果,因为此时将不需要合并两个部分结果。当使用并行流时,才会发生问题。
通过调用BaseStream定义的sequential()方法,可以把并行流转换成顺序流。该方法如下所示:
S sequential()
一般来说,可以根据需要,使流在并行流和顺序流之间切换。
使用并行执行时,关于流还有一点需要记住:元素的顺序。流可以是有序的,也可以是无序的。一般来说,如果数据源是有序的,那么流也将是有序的。。但是,在使用并行流的时候,有时候允许流是无序的可以获得性能上的提升。当并行流无序时,流的每个部分都可以被单独操作。而不需要与其他部分协调。当操作的顺序不重要时,可以调用如下所示的unordered()方法来指定无序行为:
S unordered()
另外一点:forEach()方法不一定保留并行流的顺序。如果在对并行流的每个元素执行操作时,也希望保留顺序,可以考虑使用forEachOrdered()方法。它的用法与forEach()一样。
很多时候,将一个流的元素映射到另一个流很有帮助。例如,对于一个包含由姓名、电话号码和电子邮件地址构成的数据库的流,可能值映射到另一个流的姓名和电子邮件地址部分。另一个例子是,希望对流中的元素应用一些转换。为此,可以把转换后的元素映射到一个新流。因为映射操作十分常用,所以流API为它们提供了内置支持。最具一般性的映射方法是map(),如下所示:
<R> Stream<R> map(Function<? super T,? extends R> mapFunc)
,其中,R指定新流的元素类型,T指定指定流的元素类型,mapFunc是完成映射的Function实例,映射函数必须是无状态和不干预的。因为map()方法会返回一个新流,所以它是中间方法。
Function是java.util.function包中声明的一个函数式接口,其声明如下所示:
Function<T,R>
在map()中使用时,T是元素类型,R是映射的结果类型。Function定义的抽象方法如下所示:
R apply(T val)
其中,val是对被映射对象的引用。映射的结果将被返回。
下面是使用map()方法的一个简单的例子。这是前一个示例程序的变体。与前例一样,这个程序计算ArrayList中值的平方根的乘积。但是,在这个版本中,元素的平方根被首先映射到一个新流。然后,使用reduce()方法来计算乘积。
//Map one stream to another. import java.util.ArrayList; import java.util.stream.Stream; class StreamDemo4 { public static void main(String[] args) { //A list of double values. ArrayList<Double> myList = new ArrayList<>(); myList.add(7.0); myList.add(8.0); myList.add(10.0); myList.add(24.0); myList.add(17.0); myList.add(5.0); //Map the square root of the elements in myList to a new stream. Stream<Double> sqrtRootStrm = myList.stream().map( (a)->Math.sqrt(a) ); //Fina the product of the square roots. double productOfSqrRoots = sqrtRootStrm.reduce(1.0,(a,b)->a*b); System.out.println("Product of square roots is "+productOfSqrRoots); } }
输出与前面的相同。这个板与与前一个版本的区别在于,转换(即计算平发根)发生在映射过程而不是缩减过程中。因此,可以使用带两个参数的reduce()版本来计算乘积,因为这里不需要提供单独的合并器函数。
下面这个例子使用map()创建一个新流,其中包含原始流中选定的字段。在本例中,原始流包含NamePhoneEmail类型的对象,这类对象包含姓名、电话号码和电子邮件地址。然后,程序只将姓名和电话号码映射到NamePhone对象的新流中。电子邮件地址将被丢弃。
//Use map() to create a new stream that contains only
//selected aspects of the original stream.
class NamePhoneEmail {
String name;
String phoneNum;
String email;
public NamePhoneEmail(String name, String phoneNum, String email) {
this.name = name;
this.phoneNum = phoneNum;
this.email = email;
}
}
class NamePhone {
String name;
String phoneNum;
public NamePhone(String name, String phoneNum) {
this.name = name;
this.phoneNum = phoneNum;
}
}
import java.util.ArrayList; import java.util.stream.Stream; class StreamDemo5 { public static void main(String[] args) { //A list of names,phone numbers,and e-mail address. ArrayList<NamePhoneEmail> myList = new ArrayList<>(); myList.add(new NamePhoneEmail("Larry","555-5555","1@qq.com")); myList.add(new NamePhoneEmail("Lames","555-4444","2@qq.com")); myList.add(new NamePhoneEmail("Mary","555-3333","3@qq.com")); System.out.println("Original values in myList: "); myList.stream().forEach((a)->{ System.out.println(a.name+" "+a.phoneNum+" "+a.email); }); System.out.println(); //Map just the names and phone numbers to a new stream. Stream<NamePhone> nameAndPhone = myList.stream().map((a)->new NamePhone(a.name,a.phoneNum)); System.out.println("List of name and phone numbers: "); nameAndPhone.forEach((a)->{ System.out.println(a.name+" "+a.phoneNum); }); /** * 输出: * Original values in myList: * Larry 555-5555 1@qq.com * Lames 555-4444 2@qq.com * Mary 555-3333 3@qq.com * * List of name and phone numbers: * Larry 555-5555 * Lames 555-4444 * Mary 555-3333 */ } }
因为可以把多个中间操作放到管道中,所以很容易创建非常强大的操作。例如,下面的语句使用filter()和map()方法产生一个新流,其中只包含名为"James"的元素的姓名个电话号码:
Stream<NamePhone> nameAndPhone = myList.stream().filter((a)->a.name.equals("James")).
map((a)->new NameAndPhone(a.name,a.phoneNum));
在创建数据库风格的查询时,这种过滤操作十分常见。随着使用流API的经验增长,这种链式操作可以用来在数据流上创建非常复杂的查询、合并和选择操作。
除了刚才描述的版本,map()方法还有另外三种版本,它们返回基本类型的流,如下所示:
IntStream mapToInt(ToIntFunction<? super T> mapFunc)
LongStream mapToLong(ToLongFunction<? super T> mapFunc)
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapFunc)
每个mapFunc必须实现由指定接口定义的抽象方法,并返回指定的类型的值。例如,ToDoubleFunction指定了applyAsDouble(T val)方法,该方法必须将其参数的值作为double类型返回。
下面展示了一个使用基本类型流的例子。首先创建一个double值的ArrayList,然后使用stream()和mapToInt()方法创建一个IntStream,使其包含不小于每个double值的最小整数。
//Map a Stream to an IntStream import java.util.ArrayList; import java.util.stream.IntStream; class StreamDemo6 { public static void main(String[] args) { //A list of double values. ArrayList<Double> myList = new ArrayList<>(); myList.add(1.1); myList.add(3.6); myList.add(9.2); myList.add(4.7); myList.add(12.1); myList.add(5.0); System.out.print("Original values in myList: "); myList.stream().forEach((a)->{ System.out.print(a+" "); }); System.out.println(); //Map the ceiling of the elements in myList to an IntStream. IntStream cStrm=myList.stream().mapToInt((a)->(int)Math.ceil(a)); System.out.print("The ceilings of the values in myList: "); cStrm.forEach((a)->{ System.out.print(a+" "); }); /** * 输出: * Original values in myList: 1.1 3.6 9.2 4.7 12.1 5.0 * The ceilings of the values in myList: 2 4 10 5 13 5 */ } }
mapToInt()方法产生的流包含不小于myList中原始元素的最小整数。
有必要指出的是:流API还提供了支持flat map的方法,包括flatMap()、flatMapToInt()、flatMapToLong()和flatMapToDouble()。设计flat map方法,是为了处理原始流中的每个元素映射到结果中的多个元素的情况。
如前面的例子所示,可以从集合中获得流,并且这种做法十分常见。但是,有时候需要执行反操作:从流中获得集合。为了执行这种操作,流API提供了collect()方法。它有两种形式,首先使用如下形式:
<R,A> R collect(Collector<? super T,A,R> collectorFunc)
其中,R指定结果的类型,T指定调用流的元素类型。内部累积类型由A指定。collectorFunc指定收集过程如何执行。collect()方法是一个终端方法。
Collector接口是在java.util.stream包中声明的,如下所示:
interface Collector<T,A,R>
T、A、R的含义与上述相同。本篇使用Collectors类提供的两个预定义收集器。Collectors类包含在java.util.stream包中。
Collectors类定义了许多可以直接使用的静态收集器方法。我们将使用的两个是toList()和toSet(),如下所示:
static <T> Collector<T,?,List<T>> toList()
static <T> Collector<T,?,Set<T>> toSet()
toList()方法返回的收集器可用于将元素收集到一个List中,toSet()方法返回的收集器可以用于把元素收集到一个Set中。例如把元素收集到List中,可以像下面这样调用collect()方法:
collect(Collectors.toList())
下面的程序演示了前面介绍的内容。它修改了前一节的示例,将姓名个电话号码分别收集到一个List中和一个Set中。
import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; class StreamDemo7 { public static void main(String[] args) { //A list of names,phone numbers,and e-mail address. ArrayList<NamePhoneEmail> myList = new ArrayList<>(); myList.add(new NamePhoneEmail("Larry","555-5555","1@qq.com")); myList.add(new NamePhoneEmail("Lames","555-4444","2@qq.com")); myList.add(new NamePhoneEmail("Mary","555-3333","3@qq.com")); //Map just the names and phone numbers to a new stream. Stream<NamePhone> nameAndPhone = myList.stream().map( (a)->new NamePhone(a.name,a.phoneNum) ); //Use collect to create a List of the names and phone numbers. List<NamePhone> npList = nameAndPhone.collect(Collectors.toList()); System.out.println("Names and phone numbers in a List:"); for (NamePhone e:npList){ System.out.println(e.name+": "+e.phoneNum); } //Obtain another mapping of the names and phone numbers. nameAndPhone=myList.stream().map( (a)->new NamePhone(a.name,a.phoneNum) ); //Now,create a Set by use of collect(). Set<NamePhone> npSet = nameAndPhone.collect(Collectors.toSet()); System.out.println("\nNames and phone numbers in a Set:"); for (NamePhone e:npSet){ System.out.println(e.name+": "+e.phoneNum); } /** * 输出: * Names and phone numbers in a List: * Larry: 555-5555 * Lames: 555-4444 * Mary: 555-3333 * * Names and phone numbers in a Set: * Lames: 555-4444 * Larry: 555-5555 * Mary: 555-3333 */ } }
在程序中,下面的一行代码通过toList(),将姓名和电话号码收集到一个List中:
List<NamePhone> npList = nameAndPhone.collect(Collectors.toList());
执行完这行代码后,npList引用的集合可以像其他任何List集合一样使用。例如,可以使用for-each风格的for循环遍历该集合。
通过collect(Collectors.toSet())创建Set的方法与此相同。将数据从集合移到流中,以及将数据从流移回集合的能力,是流API的一个强大特性。这允许通过流来操作集合,然后把流重新打包成集合。此外,条件合适的时候,流操作可以并发发生。
前面的例子中使用的collect()版本非常方便,也是非常常用的一个版本,但是还有另一个版本,可以对收集过程实施加更多控制。该版本如下所示:
<R> R collect(Supplier<R> target,BiConsumer<R,? super T> accumulator,BiConsumer<R,R> combiner)
这里,target指定如何创建用于保存结果的对象。例如,要使用一个LinkedList作为结果集合,需要指定其构造函数。accumulator函数将一个元素添加到结果中,而combiner函数合并两个部分结果。因此,这些函数的工作方式与在reduce()中类似。它们都必须是无状态和不干预的,并且必须具有关联性。
注意,target参数的类型是Supplier。Supplier是java.util.function包中声明的一个函数式接口,指定了如下所示的抽象方法accept():
void accept(T obj,U obj2)
这个方法对obj和obj2执行某种类型的操作。对于accumulator,obj指定目标集合,obj2指定要添加到该集合的元素。对于combiner,obj与obj2指定两个将被合并的集合。
使用这个版本的collect()方法时,在前面的程序中,可以使用一个LinkedList作为目标,如下所示:
LinkedList<NamePhone> npList = nameAndPhone.collect(()->new LinkedList(),
(list,element)->list.add(element),
(listA,listB)->listA.addAll(listB)
)
注意,collect()的第一个参数是一个lambda表达式,它返回一个新的LinkedList。第二个参数使用标准的集合方法add(),将一个元素添加到链表中。第三个参数使用addAll()方法,将两个链表合并起来。注意,可以使用LinkedList中定义的任何方法将一个元素添加到链表中。例如,可以使用addFirst()方法,将元素添加到链表的开头,如下所示:
(list,element)->list.addFirst(element)
并不总是需要collect()方法的参数指定一个lambda表达式。通常,方法和/或构造函数引用就足够了。例如,对于前面的程序,下面的程序会创建一个包含nameAndPhone流中所有元素的HashSet:
HashSet<NamePhone> npSet = nameAndPhone.collect(HashSet::new,HashSet::add,HashSet::addAll);
注意,第一个参数指定了HashSet构造函数引用,第二个和第三个参数指定了对HashSet的add()和addAll()方法的方法引用。
最后一点:用流API的术语来说,collect()方法执行所谓的可变缩减操作。这是因为,这个缩减操作的结果是一个可变(即可以修改)的存储对象,例如集合。
虽然流不是数据存储对象,但是仍然可以使用迭代器来遍历其元素,就如同使用迭代器编辑集合中的元素一样。流AIP支持两类迭代器。一类是传统的Iterator,另一类是JDK8新增的Spliterator。在使用并行流的一些场合中,Spliteartor提供了极大便利。
如前所述,可以对流使用迭代器,正如对集合使用迭代器一样。迭代器是实现了java.util包中声明的Iterator接口的对象。它的两个关键方法是hasNext()和next()。如果还有要迭代的元素,hasNext()方法返回true,否则返回false。next()方法返回迭代中的下一个元素。
注意
JDK8添加了处理基本类型流的其他迭代器类型:PrimitiveIterator、PrimitiveIterator.OfDouble、PrimitiveIterator.OfLong和PrimitiveOfInt。这些迭代器都扩展了Iterator接口,并且使用方式与直接基于Iterator的那些迭代器相同。
要获得流的迭代器,需要对流调用iterator()方法。Stream使用的iterator()方法。Stream使用的iterator()版本如下所示:
Iterator<T> iterator()
其中,T指定了元素类型(基本类型流返回对应基本类型的迭代器)。
下面的程序演示了如何遍历一个流的元素。这里遍历了ArrayList中的字符串,但是过程对于其他类型的流来说是相同的。
//Use an iterator with a stream. import java.util.ArrayList; import java.util.Iterator; import java.util.stream.Stream; class StreamDemo8 { public static void main(String[] args) { //Create a list of Strings. ArrayList<String> myList = new ArrayList<>(); myList.add("Alpha"); myList.add("Beta"); myList.add("Gamma"); myList.add("Delta"); myList.add("Phi"); myList.add("Omega"); //Obtain a Stream to the array list. Stream<String> myStream = myList.stream(); //Obtain an iterator to the stream. Iterator<String> itr=myStream.iterator(); //Iterate the elements in the stream. while (itr.hasNext()){ System.out.println(itr.next()); } } /* 输出: Alpha Beta Gamma Delta Phi Omega */ }
Spliterator可以代替Iterator,在涉及并行处理时更加方便。一般来说,Spliterator要比Iterator更复杂。Spliterator定义了几个方法,但是我们只需要使用三个。第一个是tryAdvance(),它对下一个元素执行操作,然后推进迭代器,如下所示:
boolean tryAdvance(Consumer<? super T> action)
其中,action指定了在迭代器中的下一个元素上执行的操作。如果有下一个元素,tryAdvance()方法会返回true,否则返回false。如本篇前面所述,Comsumer声明了一个叫做accept()的方法,它接受一个类型为T的元素作为参数,并返回void。
当没有更多元素需要处理时,tryAdvance()方法返回false,所以迭代循环结构变得非常简单,例如:
while(splitItr.tryAdvance(//perform action here));
只要tryAdvance()返回true,就对下一个元素执行操作。当tryAdvance()返回false时,迭代就完成了。可以看到,tryAdvance()方法将Iterator提供的hasNext()和next()方法的作用合并到了一个方法中,所以提高了迭代过程的效率。
下面对前面的程序进行修改,使用Apliterator代替Iterator:
//Use a Spliterator import java.util.ArrayList; import java.util.Spliterator; import java.util.stream.Stream; class StreamDemo9 { public static void main(String[] args) { //Create a list of Strings. ArrayList<String> myList = new ArrayList<>(); myList.add("Alpha"); myList.add("Beta"); myList.add("Gamma"); myList.add("Delta"); myList.add("Phi"); myList.add("Omega"); //Obtain a Stream to the array list. Stream<String> myStream = myList.stream(); //Obtain a Spliterator Spliterator<String> splitItr = myStream.spliterator(); //Iterate the elements of the stream. while (splitItr.tryAdvance((n)-> System.out.println(n))); } }
输出与前面相同。
有些时候,可能想将各个元素作为一个整体来应用操作,而不是一次处理一个元素。对于这种情况,Spliterator提供了forEachRemaining()方法,如下所示:
default void forEachRemaining(Consumer<? super T> action)
这个方法对每个未处理的元素应用action,然后返回。例如,对于前面的程序,下面的语句将显示流中剩余的字符串:
splitItr.forEachRemaining((n)->System.out.println(n));
注意,使用这个方法时,不需要提供一个循环来处理一个元素。这是Spliterator的又以优势。
Spliterator的另一个值得注意的方法是trySplit()。它将被迭代的元素划分成两部分,返回其中一部分的新Spliterator,另一部分则通过原来的Spliterator访问。该方法如下所示:
Spliterator<T> trySplit()
如果无法拆分调用Spliterator,返回null。否则,返回对拆分后的一部分的引用。例如,下面对前面的程序进行了修改,以演示trySplit()方法:
//Demonstrate trySplit(). import java.util.ArrayList; import java.util.Spliterator; import java.util.stream.Stream; class StreamDemo10 { public static void main(String[] args) { //Create a list of Strings. ArrayList<String> myList = new ArrayList<>(); myList.add("Alpha"); myList.add("Beta"); myList.add("Gamma"); myList.add("Delta"); myList.add("Phi"); myList.add("Omega"); //Obtain a Stream to the array list. Stream<String> myStream = myList.stream(); //Obtain a Spliterator. Spliterator<String> splitItr = myStream.spliterator(); //Now,split the first iterator. Spliterator<String> splitItr2=splitItr.trySplit(); //If splitItr could be split,use splitItr2 first. if(splitItr2!=null){ System.out.println("Output from splitItr2: "); splitItr2.forEachRemaining((n)-> System.out.println(n)); } //Now,use the splitItr. System.out.println("\nOutput from splitItr: "); splitItr.forEachRemaining((n)-> System.out.println(n)); /** * 输出: * Output from splitItr2: * Alpha * Beta * Gamma * * Output from splitItr: * Delta * Phi * Omega */ } }
虽然在这个简单的演示中,拆分Spliterator没有实际价值,但是当对大数据集执行并行处理时,拆分可能极有帮助。但是很多时候,在操作并行流时,使用其他某个Stream方法更好,而不必手动处理Spliterator主要用于所有预定义方法都不适合的场合。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。