Stream

概念

流是JavaAPI新成员,允许开发者以声明性方式处理数据集合。流与集合的区别在于:

①集合是数据结构,其主要目的是以特定的时间/空间复杂度存储和访问元素。而流的目的在于表达计算。

通俗来讲集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,它包含数据结构中目前所有的值,集合中的每个元素都得先明确计算出来后才能添加到集合中。

流则是概念上的固定数据结构,其元素数量则是按需计算的。

②和迭代器类似,流只能遍历一次。遍历完之后,流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍。

③Collection接口需要用户去做迭代,这称为外部迭代。而Streams库使用内部迭代,API已经帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。Streams库的内部迭代可以自动选择一种适合当前硬件的数据表示和并行实现。

最后再说一下java.util.stream.Stream中定义的操作。目前为两大类:

①中间操作:中间操作会返回另一个流,这让多个操作可以连接起来形成一个查询,可以在终端操作时一次性全部处理。注意,必须在流的流水线上触发一个终端操作,否则中间操作不会执行任何处理。

②终端操:从流的流水线生成结果且结果可以是除流之外的任何值。

因此流的组成可以分为3部分:一个数据源来执行一个查询、一个中间操作链形成一条流的流水线、一个终端操作来执行流水线并能生成结果。

其实流的作用,说白了就是从数据源中“筛选”出符合要求的元素,其重点在于“筛选”动作,“筛选”的逻辑行为是通过行为参数化来达成,内部循环则是API自己内部完成。

筛选

谓词筛选:filter方法会接受一个谓词(一个返回boolean类型的函数)来作为参数,并返回一个包括所有符合谓词的元素的流。

去重筛选:distinct方法返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流。

截短筛选:limit(n)方法返回一个不超过给定长度n的流。如果流是有序的则最多会返回前n个元素,如果流是无序的则limit结果不会以任何顺序排列。

跳过筛选:skip(n)方法返回一个扔掉了前n个元素的流。如果流中元素不足n个则返回一个空流。

映射

map方法:map方法接受一个函数作为参数。这个函数会被应用到每个元素上并将其映射成一个新的元素。

flatMap方法:flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

查找

anyMatch方法:检查谓词是否至少匹配一个元素,即流中是否有一个元素能匹配给定的谓词,anyMatch方法返回一个boolean,因此是一个终端操作:if(menu.stream().anyMatch(Dish::
isVegetarian)){XXX}

allMatch方法:检查谓词是否匹配所有元素,allMatch方法的工作原理和anyMatch类似,会看看流中的元素是否都能匹配给定的谓词。

noneMatch方法:和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。anyMatch、allMatch和noneMatch这三个操作都用到了短路逻辑,类似于&&和||运算符在流中的版本。

findFirst方法:对于由List或排序好的数据列生成的流,可能想要找到第一个元素。为此有一个findFirst方法。

findAny方法:将返回当前流中的任意元素。该方法返回一个Optional实例。Optional<T>类是一个容器类,代表一个值存在或不存在。即便findAny可能什么元素都没找到,也可以通过Optional实例来避免返回null。

归约

此类查询需要将流中所有元素反复结合起来,得到一个值,这样的查询可以被归类为归约操作。

求和/最大值/最小值:reduce方法两个参数:初始值、BinaryOperator<T>。如果是求和的话,则初始值是0,而BinaryOperator将两个元素相加产生一个新值,然后Lambda反复相加每个元素,直到流被归约成一个值。

同时reduce方法还有一个重载变体,参数只有BinaryOperator,但会返回一个Optional对象:考虑流中没有任何元素的特殊情况, reduce操作无法返回其和,再加上没有初始值,这就是为什么返回Optional对象。

注意事项

①谈起求和,按照之前的做法就是for循环,然后内部通过sum + = sum + XXX的方式来完成,如果针对这种编码进行多线程改造的话,那么势必涉及到分块输入、分块求和、最后合并,麻烦,可变的累加器模式对于并行化来说是死路一条。

而reduce的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作,只需把stream()换成了parallelStream()就可以实现并行计算。

②那是不是任何一个操作都可以通过parallelStream来实现并行效果吗?

答案是不一定。这与具体操作有关。

例如map/filter这种,其从输入流中获取每一个元素,并在输出流中得到0或1个结果。这类操作都是无状态的:它们没有内部状态,可以通俗理解为这类操作只关心当前元素,其他不关心。

例如reduce、sum、max等操作需要内部状态来累积结果。但这个内部状态仅仅关心上一个操作的结果而已,因此,内部状态是很小的,是有界限的。即便元素的数量趋向于无穷大,也没事,因为我只关心上个操作的结果。

例如sort、distinct等操作与filter和map有一个关键的区别。从流中排序、去重时都需要知道先前的历史。那么,如果流里面元素数量趋向于无穷大的时候,这种情况下的内部状态是无界限的,这类操作统一叫作:有状态操作。

distinct、skip、limit、sorted、reduce都是有状态操作。

数值流

Java8引入了三个原始类型特化流接口来解决Integer/Double的装箱/拆箱:IntStream、DoubleStream、LongStream分别将流中的元素特化为int、long、double从而避免了暗含的装箱成本。

IntStream、DoubleStream、LongStream在必要时想转换成基本Stream时可以使用boxed方法。

前面我们介绍了Optional类,这是一个可以表示值存在或不存在的容器。 Optional可以用Integer、 String等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本: OptionalInt、 OptionalDouble和OptionalLong。

创建流

①由值创建流:使用静态方法Stream.of通过显式值创建一个流,可以接受任意数量的参数。例如:Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");

②由数组创建流:可以使用静态方法Arrays.stream从数组创建一个流,它接受一个数组作为参数。例如:int sum = Arrays.stream(numbers).sum();

③由文件生成流:java.nio.file.Files中的很多静态方法都会返回一个流。

④由函数生成无限流:StreamAPI提供了两个静态方法来从函数生成流:Stream.iterate、Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流,由iterate和generate产生的流会用给定的函数按需创建值,如果不加limit(n)进行限制的话,其流内元素数量可以是无穷无尽。

收集器

收集器collect本质是一个归约操作,接受各种做法作为参数,最终将流中的元素累积成一个汇总结果。具体汇总做法是通过定义新的Collector接口来实现。

一般来说,Collector会对元素应用一个转换函数并将结果累积在一个数据结构中,从而产生这一过程的最终输出。Collector接口中方法的实现决定了如何对流执行归约操作,Collectors实用类提供了很多静态工厂方法,可以方便地创建常见收集器的实例,只要拿来用就可以了。最直接和最常用的收集器是toList静态方法,它会把流中所有的元素收集到一个List中,可以总结为一句话:收集器用作高级归约。

JAVA8的Collectors预定义了很多收集器,这些预定义收集器功能大致分为3大功能:将流元素归约和汇总为一个值、元素分组、元素分区。

归约和汇总收集器

Collectors.counting():统计流中所有元素的数量。

Collectors.maxBy():计算流中的最大值,接收一个Comparator参数来比较流中的元素。

Collectors.minBy():计算流中的最小值,接收一个Comparator参数来比较流中的元素。

Collectors.summingInt():接受一个把对象映射为求和所需int的函数并返回一个收集器;该收集器在传递给普通的collect方法后即执行我们需要的汇总操作。

Collectors.summingLong():与summingInt相同。

Collectors.summingDouble():与summingInt相同。

Collectors.averagingInt():求平均数,与summingInt相同。

Collectors.averagingLong():求平均数,与summingInt相同。

Collectors.averagingDouble():求平均数,与summingInt相同。

Collectors.summarizing():这个收集器会把所有这些信息收集到一个叫作IntSummaryStatistics的类里,它提供了方便的取值( getter)方法来访问结果。该收集器会指定一个收集属性,只要该属性值为int类型即可。

Collectors.summarizingLong():这个收集器会把所有这些信息收集到一个叫作LongSummaryStatistics的类里,它提供了方便的取值( getter)方法来访问结果。该收集器会指定一个收集属性,只要该属性值为long类型即可。

Collectors.summarizingDouble():这个收集器会把所有这些信息收集到一个叫作DoubleSummaryStatistics的类里,它提供了方便的取值( getter)方法来访问结果。该收集器会指定一个属性,只要该属性值为double类型即可。

Collectors.joining():返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。

该方法有个重载版本,即joining("分隔符")版本,可以指定字符串连接的分割符。

Collectors.reducing收集器

上面预定义的收集器,都是一个可以用reducing工厂方法定义的归约过程的特殊情况而已。Collectors.reducing工厂方法是所有这些特殊情况的一般化。reducing方法需要三个参数:

①第一个参数是归约操作的起始值,也是流中没有元素时的返回值。

②第二个参数转换函数,因为该方法是对归约和汇总的抽象,因此这里指定针对哪个属性进行归约和汇总操作。

③第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值,这里放着最终归约和汇总的业务逻辑,比如求和、最大值、最小值等。

分组收集器

该类收集器的作用,类似于SQL语句中group by根据一个或多个column进行分组,只不过这里的column要换成实例的属性。

Collectors.groupingBy():传递了一个Function,我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组。分组操作的结果是一个Map,把分组函数返回的值作为映射的key,把流中所有具有这个分类值key的项目的列表list作为对应的映射值value。

多级分组收集器

Collectors.groupingBy():要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。

那么要进行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。

即groupingBy有2个参数,第一个参数是转换函数,第二个参数继续是collector类型的参数,先按照第一个参数分组,然后再按照第二个参数分组,即groupingBy嵌套groupingBy,返回的结果也是Map嵌套Map。

注意:groupingBy第二个参数是collector类型,因此,第二个参数不仅仅局限于groupingBy。

扩展:

Collectors.collectingAndThen():这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。这个收集器相当于旧收集器的一个包装, collect操作的最后一步就是将返回值用转换函数做一个映射。

分区收集器

Collectors.partitioningBy():分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,该函数称分区函数。

分区函数返回一个布尔值,这意味着得到的分组Map的键key类型是Boolean,于是它最多可以分为两组:true一组, false一组。而Value则是List。

多级分区收集器

Collectors.partitioningBy也有2个参数的版本,其参数类型与Collectors.groupingBy()一致。

收集器接口

public interface Collector<T, A, R> {

Supplier<A> supplier();

BiConsumer<A, T> accumulator();

Function<A, R> finisher();

BinaryOperator<A> combiner();

Set<Characteristics> characteristics();

}

T是流中要收集的项目的泛型。

A是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。

R是收集操作得到的对象(通常但并不一定是集合)的类型。

①supplier方法:建立新的结果容器。supplier方法必须返回一个结果为空的Supplier,也就是一个无参函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。很明显,对于将累加器本身作为结果返回的收集器。

②accumulator方法:将元素添加到结果容器。accumulator方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n-1 个项目), 还有第n个元素本身。该函数将返回void,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。

③finisher方法:对结果容器应用最终转换。在遍历完流后, finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。通常,就像ToListCollector的情况一样,累加器对象恰好符合预期的最终结果,因此无需进行转换。所以finisher方法只需返回identity函数:

④combiner方法:合并两个结果容器。combiner方法会返回一个供归约操作使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。有了这第四个方法,就可以对流进行并行归约了。它会用到Java 7中引入的分支/合并框架和Spliterator抽象。

⑤characteristics方法:返回一个不可变的Characteristics集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics是一个包含三个项目的枚举:

UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。

CONCURRENT——accumulator函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为UNORDERED,那它仅在用于无序数据源时才可以并行归约。

IDENTITY_FINISH——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值