流API

JDK8在线Api中文手册

JDK8在线Api英文手册


   在JDK8新增的许多功能中,有两个可能最为重要,分别是lambda表达式和流API。本篇介绍流API。流API的设计考虑到了lambda表达式。而且,流API有力的展示了lambda表达式带给Java的强大能力。
   虽然与lambda表达式的设计兼容性十分惹人注意,但是流API的关键一点在于能够执行非常复杂的查找、过滤和映射数据等操作。例如,使用流API时,可以构造动作序列,使其在概念上类似于使用SQL执行的数据库查询。另外,在很多时候,特别是涉及大数据集时,这类动作可以并行执行,从而提高效率。简单来说,流API提供了一种高效易于使用
的处理数据的方式。
   流API使用了Java的一些高级的功能。所以,要想完整地理解和使用流API,需要牢固地掌握泛型和lambda表达式。

1 流的基础知识

   "流"的概念:流是数据的渠道。因此,流代表了一个对象序列。流操作数据源,如数组或集合。流本身不存储数据,而只是移动数据,在移动过程中可能会对数据执行过滤、排序或其他操作。然而,一般来说,流操作本身不会修改数据源。例如,对流排序不会修改数据源的顺序。相反,对流排序会创建一个新流,其中包含排序后的结果。
   注意
   这里使用的"流"与I/O类使用的"流"不同。虽然I/O类的行为在概念上与java.util.stream中定义的流类似,但它们是不同的。因此,本篇中使用"流"这个属于时,指的是这里描述的某个流类型的对象。

1.1 流接口

   流API定义了几个流接口,包含在java.util.stream中,BaseStream是基础接口,它定义了所有流都可以使用的基本功能。它是一个泛型接口,其声明如下所示:

interface BaseStream<T,s extends BaseStream<T,S>>

   其中,T指定流中元素的类型,S指定扩展了BaseStream的流的类型。BaseStream扩展了AutoCloseable接口,所以可以使用带资源的try语句管理流。但是,一般来说,只有当流使用的数据源需要关闭时(如流连接到文件),才需要关闭流。大多数时候,例如数据源是结合的情况,不需要关闭流。表1列出了BaseStream接口声明的方法。

表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列出了其中的几个。

表2 Stream接口声明的方法举例
方 法 描 述
<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,但是基本类型流的处理基本上是相同的。

1.2 如果获取流

   获得流的方式有多种。可能最常见的是获得集合流。从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源的流。不管流是如果获得的,使用方式都与其他流一样。

1.3 一个简单的流实例

   下面的程序创建了一个叫做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);

2 缩减操作

   考虑前面示例程序中的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;})

3 使用并行流

   借助多核处理器并执行代码可以显著提高性能。然而,并行编程可能十分复杂且容易出错。流库提供的好处之一是能够可靠地并行执行一些操作。
   请求并行处理流:只需要一个并行流即可。如前所述,获得并行流的一种方式是使用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()一样。

4 映射

   很多时候,将一个流的元素映射到另一个流很有帮助。例如,对于一个包含由姓名、电话号码和电子邮件地址构成的数据库的流,可能值映射到另一个流的姓名和电子邮件地址部分。另一个例子是,希望对流中的元素应用一些转换。为此,可以把转换后的元素映射到一个新流。因为映射操作十分常用,所以流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","[email protected]"));
      myList.add(new NamePhoneEmail("Lames","555-4444","[email protected]"));
      myList.add(new NamePhoneEmail("Mary","555-3333","[email protected]"));
      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 [email protected]
       * Lames 555-4444 [email protected]
       * Mary 555-3333 [email protected]
       *
       * 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方法,是为了处理原始流中的每个元素映射到结果中的多个元素的情况。

5 收集

   如前面的例子所示,可以从集合中获得流,并且这种做法十分常见。但是,有时候需要执行反操作:从流中获得集合。为了执行这种操作,流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","[email protected]"));
      myList.add(new NamePhoneEmail("Lames","555-4444","[email protected]"));
      myList.add(new NamePhoneEmail("Mary","555-3333","[email protected]"));
     //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()方法执行所谓的可变缩减操作。这是因为,这个缩减操作的结果是一个可变(即可以修改)的存储对象,例如集合。

6 迭代器和流

   虽然流不是数据存储对象,但是仍然可以使用迭代器来遍历其元素,就如同使用迭代器编辑集合中的元素一样。流AIP支持两类迭代器。一类是传统的Iterator,另一类是JDK8新增的Spliterator。在使用并行流的一些场合中,Spliteartor提供了极大便利。

6.1 对流使用迭代器

   如前所述,可以对流使用迭代器,正如对集合使用迭代器一样。迭代器是实现了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
   */
}
6.2 使用Spliterator

   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主要用于所有预定义方法都不适合的场合。

发布了59 篇原创文章 · 获赞 20 · 访问量 3624

猜你喜欢

转载自blog.csdn.net/qq_34896730/article/details/104789811
今日推荐