流API

在jdk8新增的许多功能中,有两个可能最为重要,分别是lambda表达式和流API。流API的设计考虑到了lambda表达式。而且,流API有力的展现了lambda表达式带给java的强大的能力。
流API的关键的一点在于执行非常复杂的查找、过滤和映射数据等操作。例如,使用流API时,可以构造动作序列,使其在概念上类似于使用SQL执行的数据库查询。另外,在很多时候,特别是涉及大数据集时,这类动作可以并行执行,从而提高效率。;简单来说,流API提供了一种高效且易于使用的处理数据的方式。

一. 流API基础

、流API中“流”的概念:流是数据的渠道。因此,流代表了一个对象序列。流操作数据源,如数组或集合。流本身不存储数据,在移动过程中可能会对数据执行过滤、排序或者其他操作。然而,一般来说,流操作本身不修改数据源。例如,对流排序不会修改数据源的顺序。相反,对流排序会创建一个新流,其中包含排序后的结果。
流API中定义了几个接口,包含在java.util.stream包中。BaseStream是基础接口,它定义了所有流都可以使用的基本功能,它是一个泛型接口,其声明如下所示:
、interface BaseStream< T, S extends BaseStream< T, S >>
其中,T指定流中元素的类型,S指定扩展了BaseStream的流的类型。BaseStream扩展AutoCloseable接口,所以可以使用带资源的try语句管理流。但是,一般来说,只有当流使用的数据源需要关闭时(如流连接到文件),才需要关闭流。大多数时间,例如数据源是集合的情况,不需要关闭流。
BaseStream接口派生出了几个流接口,其中最具一般性的有Stream接口,其声明如下所示:
interface Stream< T >
其自,T指定流中元素的类型。因为Stream是泛型接口,所以可以用于所有引用类型。除了继承自BaseStream的方法,Stream接口还定义了几个自己的方法。Stream中定义的方法可以分为“终端操作”或“中间操作”两种。二者的区别很重要。终端操作会消费流。这种操作用于产生结果,例如找出流中最小的元素,或者执行某种操作,比如forEach()方法。一个流被消费以后就不能被重用。中间操作会产生另一个流。因此,中间操作可以用来创建执行一系列动作的管道。另外一点:中间操作不是立即发生的。相反,当在中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生。这种机制称为延迟行为,延迟行为让流API能够更加高效的执行。
流中另外一个关键点事,一些中间操作是无状态的,另外一些是有状态的。在无状态操作中,独立于其他元素处理每个元素。在有状态操作中,某个元素的处理可能依赖于其他元素。例如,排序是有状态操作,因为元素的顺序依赖其他元素的值。因此,sorted()方法是有状态的。
然而,对于无状态谓词的元素过滤是无状态的,因为每个元素都是被单独处理的。因此,filter()方法是(并且应该是)无状态的。当需要并行处理流时,无状态与有状态的区别尤为重要,因为有状态操作可能需要几次处理才能完成。
因为Stream操作的是对象引用,所以不用直接操作基本类型。为了处理基本流类型,流API定义了以下接口:
DoubleStream、IntStream、LongStream
这些流都扩展了BaseStream,并且具有类似于Steam的动作,只不过他们操作的是jib 类型,而不是引用类型。
首先看一个流使用的例子,下面的程序创建了一个叫myList的ArrayList,用于存储整数集合(自动装箱为Integer引用类型)。然后,获得一个使用myList作为源的流。最后,演示了各种流操作。

class SteamDemo{
    public static void main(String[] args)
    {
        ArrayList<Integer> myList = new ArrayList<>();
        myList.add(7);
        myList.add(18);
        myList.add(10);
        myList.add(24);
        myList.add(5);
        Stream<Integer> myStream = myList.stream();
        Optional<Integer> minVal = myStream.min(Integer::compare);
        if(minVal.isPresent())
            System.out.println("Minimun value:"+minVal.get());
        //must obtain a new stream because 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("Maximun 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.println("sorted stream: ");
        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.println("odd values:");
        oddVals.forEach((n)->System.out.println(n + " "));
        System.out.println();
        //Display only the odd values that are greater than 5.
        Notice that two filter operations are pipelined.
        oddVals = myList.stream().filter((n) -> (n%2 == 1)).filter((n) -> n>5);
        System.out.println("odd values greater than 5: ");
        oddVals.forEach((n) -> System.out.print(n + " "));
        System.out.println();
    }   
}

forEach()方法是终端操作,filter()方法是中间操作。

二. 缩减操作

前面示例程序中的min()和max()方法,都是终端操作,基于流中的元素返回结果。用流API的术语来说,它代表了缩减操作,因为每个操作都将一个流缩减为一个值——对于这两中操作就是最小值和最大值。流API将这两种操作称为特别缩减,因为它们执行了具体的操作。除了min()和max()方法,还存在其他特别缩减操作,例如统计流中元素个数的count()方法。然而,流API泛化了这种概念,提供了reduce()方法。通过使用reduce()方法,可以基于任意条件,从流中返回一个值。根据定义,缩减操作都是终端操作。
首先看Stream中定义的两个reduce()方法:
Optional< T > reduce(BinaryOperator< T > accumulator)
T reduce(T identityVal, BinaryOperator 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)
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()

class StreamDemo{
    public static void main(String[] args)
    {
        ArrayList myList = new ArrayList<>();
        myList.add(7);
        myList.add(18);
        myList.add(10);
        myList.add(24);
        myList.add(5);
        //two ways to obtian the integer produce 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);
    }
}

在程序中,reduce()方法德尔两次使用得到了相同的结果。第一个版本的reduce()方法使用lambda表达式来计算两个值得乘积。在本例中,因为流中包含Integer值,所以在乘法计算过程中会自动拆箱Integer对象,然后在返回结果时会自动重新装箱。两个值分别代表累积结果中的当前值和流中的下一个元素。最终结果放在一个Optional类型的对象中并返回。通过对返回的对象调用get()方法,可以获得这个值。
在第二个版本中,显示指定了单位值,对于乘法而言就是1。注意,结果作为元素类型的对象返回,在本例中就是一个Integer对象。
虽然对于示例而言,简单的操作很有用,如乘法操作,但是缩减操作不限于此。例如,对于前面的程序,以下代码可以获得偶数值的乘积:

int evenProduct = myList.stream().reduce(1,(a,b) ->{
    if(b%2 == 0) 
        return a*b;
    else 
        return a;
});

特别注意lambda表达式。如果b是偶数,就返回a*b;否则,返回a.前面已经介绍过,之所以可以这么做,是因为a保存了当前结果,而b保存了下一个元素。

三、使用并行流

借助多核处理器并行执行代码可以显著提高性能。因此,并行编程已成为现代程序员工作的重要部分。然而,并行编程可能十分复杂且容易出错。流库提供的好处之一是能够轻松可靠地并行执行一些操作。
请求并行处理流十分简单,只需要使用一个并行流即可。获得并行流的一种方法是使用collection定义的parallelStream()方法。另一种方法是对顺序流调用parallel()方法。parallel()方法由BaseStream定义,该方法基于调用自己的顺序流,返回一个并行流(如果调用该方法的流已经是一个并行流就返回该冰箱流)。当然,需要理解的是,即对于并行流,也只有在环境支持的的情况下才可以实现并行处理。
在前面的程序中,如果把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的操作必须不同。下面的程序,使用reduce()方法的合并器版本,计算列表中每个元素的平方根的乘积。

class StreamDemo3{
    public static void main(String[] args)
    {
        ArrayList<Double> myList = new ArrayList<Double>;
        myList.add(7.0);
        myList.add(18.0);
        myList.add(10.0);
        myList.add(24.0);
        myList.add(5.0);
        double produceOfSqrtRoots = myList.parallelStream().reduce(1.0,(a,b)->a*Math.sqrt(b),(a,b)->a*b);
        System.out.println("product of square roots: "+productOfSqrtRoots);
    }
}

注意,累加器函数将两个元素平方根相乘,但是合并器则将部分结果相乘。因此,这两个函数是不同的。不止如此,对于这种计算,这两个函数必须不同,结果才会正确。例如,如果尝试使用下面的语句来获得元素的平方根的乘积,将会发生错误:
double produceOfSqrtRoots = myList.parallelStream().reduce(1.0,(a,b)->a*Math.sqrt(b));
在这个版本的reduce()方法中,累加器和合并器函数是同一个函数。这将导致错误,因为当合并两个部分结果时,相乘的是它们的平方根,而不是部分结果自身。
值得注意的是,上面对reduce()方法的调用中,如果将流改为顺序流,那么操作将得到正确的结果,因为此时将不需要合并两个部分结果。当使用并行流时,才会发生问题。
通过调用BaseStream定义的sequence()方法,可以把并行流转换为顺序流。该方法如下所示:
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()方法计算乘积。

class StreamDemo4{
    public static void main(String[] args)
    {
        ArrayList<Double> myList = new ArrayList<>();
        myList.add(7.0);
        myList.add(18.0);
        myList.add(10.0);
        myList.add(24.0);
        myList.add(5.0);
        //map the square root of the element in mylist to a new stream.
        Stream<Double> sqrtRootStrm = myList.stream((a)->Math.sqrt(a));
        double productOfsqrRoots = sqrtRootStrm.reduce(1.0,(a,b)->a*b);
        System.out.println("product of square roots is "+productOfsqrRoots);
    }
}

下面这个例子是使用map()创建一个新流,其中包含原始流中选定的字段。在本例中,原始流包含NamePhoneEmail类型的对象,这类对象包含姓名、电话号码和电子邮件地址。然后,程序只将姓名和电话号码映射到NamePhone对象的流中。电子邮件地址将被丢弃。

class NamePhoneEmail{
    String name;
    String phonenum;
    String email;
    NamePhoneEmail(String n, String p, String e)
    {
        name = n;
        phonenum = p;
        email = e;
    }
}
class NamePhone{
    String name;
    String phonenum;
    NamePhoneEmail(String n, String p)
    {
        name = n;
        phonenum = p;
    }
}
class StreamDemo5{
    public static void main(String[] args)
    {
        ArrayList<NamePhoneEmail> myList = new ArrayList<>();
        myList.add(new NamePhoneEmail("Larry","555-5555","Larry@HerbSchildt.com"));
        myList.add(new NamePhoneEmail("James","555-4444","James@HerbSchildt.com"));
        myList.add(new NamePhoneEmail("Mary","555-3333","Mary@HerbSchildt.com"));
        System.out.println("original valujes in myList: ");
        myList.stream.forEach((a->{
            System.out.println(a.name+" "+a.phone+" "+a.email);
        }));
        System.out.printl();
        //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 names and phone numbers");
        nameAndPhone.forEach((a)->{
            System.out.println(a.name+" "+a.phonenum);
        });
    }   
}

因为可以把多个中间操作放在管道中,所以很容易创建非常强大的操作。例如,下面的语句使用filter()和map()方法产生了一个新流,其中包含名为“James”的元素的姓名和电话号码:

Stream<NamePhone> nameAndPhone = myList.stream().filter((a)->a.name.equals("James")).map((a)-new NamePhone(a.name,a.phonenum));

随着使用流API的经验增多,您将发现,这种链式操作可以用来在数据流上创建非常复杂的查询、合并和选择操作。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值