Part 1

Java8新增的功能中,要数lambda表达式和流API最为重要了。这篇文章主要介绍流API的基础,也是流API系列的第一部分,话不多说,直奔主题。

什么是流API? 它能做一些什么?

我们应该知道(绝对知道~)API是一个程序向使用者提供的一些方法,通过这些方法就能实现某些功能。所以对于流API来说,重点是怎么理解“流”这个概念,所谓的流:就是数据的渠道,所以,流代表的是一个对象的序列。它和Java I/O类里使用的"流"不同。虽然在概念上与java.util.stream中定义的流是类似的,但它们是不同的。流API中的流是描述某个流类型的对象。

流API中的流操作的数据源,是数组或者是集合。它本身是不存储数据的,只是移动数据,在移动过程中可能会对数据进行过滤,排序或者其它操作。但是,**一般情况下(绝大数情况下),流操作本身不会修改数据源。**比如,对流排序不会修改数据源的顺序。相反,它会创建一个新的流,其中包含排序后的结果。

从一个简单的例子,体验流API的强大与优雅

这个简单的Demo,主要是对一个由1-6乱序组成的List对应的流进行操作,然后通过这个流,就可以获取到列表里面最大最小值,排序,过滤某些元素等等的操作。并且这此操作不会改变原List里面的数据。Demo里面需要注意的地方就是流API里面的“终端操作”和“中间操作”的区别:其实也很简单,终端操作会消费流,一个被消费过的流是不能被再次利用的,但我们在实际应用的时候,并不会受到太大的影响。

首先创建一个list并初始化测试数据。

List<Integer> list = new ArrayList<>();list.add(4);list.add(3);list.add(6);list.add(1);list.add(5);list.add(2);
最大值和最小值

首先通过stream()方法获取List对应的流,如果你对Java8的集合框架有一定的了解,你应该知道stream()是由Collection接口提供的。然后就可以通过min()获取流中的最小值了,当然这个流中的最小值肯定也是List里面的最小值。

不过需要注意的一点,因为min()是一个终端操作,所以这个流是不可以再用了,因此我们需要通过stream()重新生成一个流,(但这其实并不影响我们的实际生产的:①方法功能单一原则 ②还有其它很多很强大的方法组合能让你实现各种功能啊。)

//最小值
System.out.println("List中最小的值为:");
list.stream().min(Integer::compareTo).ifPresent(i -> System.out.println(i));
//最大值
System.out.println("List中最大的值为:");
list.stream().max(Integer::compareTo).ifPresent(i -> System.out.println(i));
排序和遍历

通过上面的讲解,相信这个已经难不了你了,sorted()方法是用于排序的,它的一个重载方法可以接收一个Comparator类型的参数,让你自定义你的排序规则。forEach方法就遍历。

//排序
System.out.println("将List流进行排序:");
list.stream().sorted().forEach(i -> System.out.print(i + " "));
过滤

filter()是基于一个谓词过滤流,它返回一个只包含满足谓词的元素的新流。它的参数形式是Predicate,是在java.util.function包下的泛型函数式接口。并且filter是一个中间操作,而且还可以同时存在多个filter。这里的两个过滤器,我们都传递了lambda表达式。

System.out.println("过滤List流,只剩下那些大于0并且小于4的元素");
list.stream().filter(i -> i > 0).filter(i -> i < 4).sorted().forEach(i -> System.out.print(i + " "));

小结一下

其实基本的流API使用就是这么简单,结合lambda表达式后,一切都变得特别清淅。这个简单的Demo展示了一些基础的功能,它或许就扩展了你操作数组或者集合框架的思路,让你操作集合和数组,变得更加的容易,简单和高效。当然流API的的功能肯定不止这一点点,让我们接着学习。

Part 2

继续探索流API的高级功能之前,我们先从接口级别全面了解一下流API,这个对于我们来说是至关重要的。接下来,我给大家准备了一张流API关键知识点的UML图。但是大家只需要花一两分钟,整理看一下就可以了,不需要记住,先有个印象,后面我给大家讲解一些关键的方法。

流API UML

我先整体介绍一下:流API定义的几个接口,都是在java.util.stream包中的.其中上图中的BaseStream接口是最基础的接口,它提供了所有流都可以使用的基本功能:

public interface BaseStream<T, S extends BaseStream<T, S>>extends AutoCloseable {...
}

由这个接口的定义我们得知,BaseStream是一个泛型接口,它有两个类型参数T和S, 其中T指定了流中的元素的类型,并且由 <S extends BaseStream<T,S>> 可以知道S必须为BaseStream或BaseStream子类(换句话说,就是S必须是扩展自BaseStream的)。

BaseStream继承了AutoCloseable接口。这个接口主要是简化了关闭资源的操作。但是像平时我们操作的集合或数组,基本上都不会出现关闭流的情况。

//由BaseStream接口派生出的流接口包括IntStream ,LongStream,DoubleStream ,Stream<T>
public interface IntStream extends BaseStream<Integer, IntStream> {}
public interface LongStream extends BaseStream<Long, LongStream> {}
public interface DoubleStream extends BaseStream<Double, DoubleStream> {}
//这是最具代表性的接口
public interface Stream<T> extends BaseStream<T, Stream<T>> {...
}

由于Stream接口是最具代表性的,所以我们就选择它来讲解,其实在我们学完Stream接口,其它的三个接口,在使用上基本是一致的了。

我们回想一下:在上一个Demo中我们通过集合框架的stream()方法,就能返回一个流了,它的返回类型就是Stream,比如我们Stream,由此得知,Stream接口里的类型参数T就是流中的元素的类型。

BaseStream详解

首先看下它都提供了什么方法:

  • Iterator iterator(): 获得流的迭代器,并返回对该迭代器的引用(终端操作)。
  • Spliterator spliterator(): 获取流的spliterator(并行迭代器),并返回其引用(终端操作)。
  • boolean isParallel(): 如果调用流是一个并行流,则返回true;如果调用流是一个顺序流,则返回false。
  • S sequential(): 基于调用流,返回一个顺序流。如果调用流已经是顺序流了,就返回该流。(中间操作)。
  • S parallel(): 基于调用流,返回一个并行流。如果调用流已经是并行流了,就返回该流。(中间操作)。
  • S unordered(): 基于调用流,返回一个无序流。如果调用流已经是无序流了,就返回该流。(中间操作)。
  • **S onClose(Runnable closeHandler):**返回一个新流,closeHandler指定了该流的关闭处理程序,当关闭该流时,将调用这个处理程序。(中间操作)。
  • void close(): 从AutoCloseable继承来的,调用注册关闭处理程序,关闭调用流(很少会被使用到)。

“终端操作”&“中间操作”

BaseStream接口里面的很多方法都在最后标识了(终端操作)和(中间操作),它们之间的区别是非常重要的。

  • 终端操作 会消费流,这种操作会产生一个结果的,比如上面的 iterator()和 spliterator(),以及上一篇中提到的min()和max(),或者是执行某一种操作,比如上一篇的forEach(),如果一个流被消费过了,那它就不能被重用的。
  • 中间操作 中间操作会产生另一个流。因此中间操作可以用来创建执行一系列动作的管道。一个特别需要注意的点是:中间操作不是立即发生的。相反,当在中间操作创建的新流上执行完终端操作后,中间操作指定的操作才会发生。所以中间操作是延迟发生的,中间操作的延迟行为主要是让流API能够更加高效地执行。

"中间操作"的状态

流的中间操作,可以为分 无状态操作有状态操作两种,在无状态操作中,在处理流中的元素时,会对当前的元素进行单独处理。比如:谓词过滤操作,因为每个元素都是被单独进行处理的,所有它和流中的其它元素无关,因此被称为无状态操作;而在有状态操作中,某个元素的处理可能依赖于其他元素。比如查找最小值,最大值,和排序,因为他们都依赖于其他的元素。因此为称为有状态操作。

当需要进行并行处理流时,有状态的操作和无状态的区别是非常重要的,因为有状态操作可能需要几次处理才能完成。另外,指出一点,如果大家了解泛型的话,应该知道,泛型的类型参数只能是引用类型,因此Stream操作的对象只能是引用类型的,不能用于基本类型。当然官方早已考虑到这一点了,前面你们看到的IntStream,LongStream,DoubleStream就是官方给我们提供的处理基本类型的流了。

Stream详解

steam提供的方法:

  • Stream filter(Predicate predicate): 产生一个新流,其中包含调用流中满足predicate指定的谓词元素(中间操作)
  • Stream map(Function mapper): 产生一个新流,对调用流中的元素应用mapper,新流中包含这些元素。(中间操作)
  • IntStream mapToInt(ToIntFunction mapper): 对调用流中元素应用mapper,产生包含这些元素的一个新IntStream流。(中间操作)
  • Stream sorted()/Stream sorted(Comparator comparator): 产生一个自然顺序排序或者指定排序条件的新流(中间操作)
  • void forEach(Consumer action): 遍历了流中的元素(终端操作)

小结一下

这一篇主要是介绍了流API的一些关键方法,和一些关键的概念,虽然稍微枯燥了一点,但是,不能否认,全面地学习流API,会让你对流API的认识会更加的深刻,所以如果时间允许,请再认真读读这一篇文章吧,当然,也可以在实践中慢慢认识它们,但是,对于这些基本概念的知识,你越早掌握,对你的益处是更加大的。

Part 3

本篇我们要讲的是流API的缩减操作。

何为缩减操作?

①都返回了一个值。 ②由一可知,他们是终端操作。

如果我们用流API的术语来形容前面这两种特性的结合体的话,它们代表了缩减操作。因为每个缩减操作都把一个流缩减为一个值,好比最大值,最小值。当然流API,把min()和max(),count()这些操作称为特例缩减。

即然说到了特例,肯定就有泛化这种概念了,他就是reduce()方法了,其实第二篇当中,他已经出现过了,只是当时我没有去强调他。

T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

Stream接口定义了三个版本的reduce(),我们先使用前面两个。

第一个版本返回的是一个T类型的对象,T代表的是流中的元素类型!第二个版本是返回一个Optional类型对象。对于这两种形式,accumulator是一个操作两个值并得到结果的函数。在第一个版本当中,identit相当于一个初始化值,identit会首先参与逻辑处理。

其中的accumulator是一个BinaryOperator的类型,他是java.util.function包中声明的函数式接口,它扩展了BiFunction函数式接口。

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {}
@FunctionalInterface
public interface BiFunction<T, U, R> {}

BiFunction接口中的apply()方法。其中R指定了结果的类型,T,U分别是第一参数的类型和第二个参数的类型,因此apply()对他的两个操作数(t,u)应用到同一个函数上,并返回结果,而对BinaryOperator来说,他在扩展 BiFunction时,指定了所有的类型参数都是相同的T,因此对于BinaryOperator函数式接口的apply来说,他也就变成了 T apply(T t, T u),此外,还有一个需要注意的地方是,在应用reduce()时,apply()的第一个参数t,包含的是一个结果,u包含的是下一个元素。在第一次调用时,将取决于使用reduce()的版本,t可能是单位值,或者是前一个元素。

缩减操作的三个约束

  • 无状态
  • 不干预
  • 关联性

无状态就是每个元素都被单独地处理,他和流中的其它元素是没有任何依赖关系的。不干预是指操作数不会改变数据源。最后,操作必须具有关联性,这里的关联性是指标准的数学含义,即,给定一个关联运算符,在一系列操作中使用该运算符,先处理哪一对操作数是无关紧要的。比如,(1 * 2) * 3 <===> 1 * (2 * 3)。

其中关联性,在并行流中,是至关重要的。 下面我用一个简单的例子带着大家实战一下泛化缩减操作reduce()的使用。

        List<Integer> list = new ArrayList<>();list.add(4);list.add(3);list.add(6);list.add(1);list.add(5);list.add(2);Optional<Integer> sum = list.stream().reduce((a,b) -> a+b);sum.ifPresent(i -> System.out.println("list的总和为:" + i));Integer sum2 = list.stream().reduce(0, (a,b) -> a+b);System.out.println("list的总和为:" + sum2);//list的偶数的积Optional<Integer> total = list.stream().reduce((a,b) -> {if(b%2 == 0) {return a*b;} else {return a;}});total.ifPresent(i -> System.out.println("list的偶数的积为:" + i));Integer total2 = list.stream().reduce(1, (a,b) -> {if(b%2 == 0) {return a*b;} else {return a;}});System.out.println("list的偶数的积为:" + total2);

小结一下

对于流的缩减操作来说,主要要知道,他只返回一个值,并且它是一个终端操作,然后还有的就是要知道缩减操作的三个约束了,其实最重要的就是无状态性和关联性了.这一小节要说的,也就这么多了,应该很容易就把他收到自己的技能树上面了。

Part 4

并行流

并行编程可谓是十分复杂并且很容易出错的,这估计就是我们绝大部分人的拦脚石。刚好Stream流库给我们解决了这个问题,在流API库里面提供了轻松可靠的并行操作。要想并行处理流相当简单,只需要使用一个并行流就可以了。

一般来说,应用到并行流的任何操作都必须是符合缩减操作的三个约束条件,无状态,不干预,关联性! 因为这三大约束确保在并行流上执行操作的结果和在顺序流上执行的结果是相同的。

我们在上一篇讲缩减操作的时候,提到了三个reduce(),但是我们只讲了两个。第三个方法只有在并行流中才会有效。

<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

在reduce()的这个版本当中,accumulator被称为累加器,combiner被称为合成器,combiner定义的函数将accumulator提到的两个值合并起来,因此,我们可以把上面的那个例子改成:

        List<Integer> list = new ArrayList<>();list.add(4);list.add(3);list.add(6);list.add(1);list.add(5);list.add(2);Integer product = list.parallelStream().reduce(0, (a,b) -> a+b, (a,b) -> a+b);System.out.println("list的总和为:" + product);

他们得到的结果还是一样的。

但是我们需要注意的是: identity会在并行过程中多次参与运算,上面的例子如果将 0 改成 1,将会根据并行数重复参与运算。

你们可能以为accumulator和combiner执行的操作是相同的,但其实他们是可以不同的,下面的例子,你们要认真看了:假设List里面有三个Integer类型的元素分别为1,2,3。

现在的需求是分别让List里面的每个元素都放大两倍后,再求积。这个需求的正确答案应该是48;

        List<Integer> lists = new ArrayList<>();lists.add(1);lists.add(2);lists.add(3);Integer products = lists.parallelStream().reduce(1, (a,b) -> a* (b * 2), (a,b) -> a * b);System.out.println("products:" + products);


此时,accumulator和combiner执行的操作是不是一定不能相同了。理解这些,对于理解并行流是非常重要的。

关于使用并行流的时候,还有一个点需要记住:如果集合中或者数组中的元素是有序的,那么对应的流也是有序的。但是在使用并行流时,有时候流是无序的就能获得性能上的提升。 因为如果流是无序的,那么流的每个部分都可以被单独的操作,而不需要与其他部分协调,从而提升性能。所以当流操作的顺序不重要的时候,可以通过BaseStream接口提供的unordered()方法把流转换成一个无序流之后,再进行各种操作。

另外一点:forEach()方法不一定会保留并行流的顺序,如果在对并行流的每个元素执行操作时,也希望保留顺序,那么可以使用forEachOrdered()方法,它的用法和forEach()是一样的。 因为在发布第一篇文章的时候,大家对forEach的反应比较大,很多人其实对forEach都有想法:比如调试难,等等。借这个机会,我谈一谈我对for&forEach的看法。

我们在访问一个数组元素的时候,最快的方式肯定是通过索引去访问的吧,而for循环遍历的时候就是通过下标进行的,所以效率那是相当的高,但是当我们的数据结构不是数组的时候,比如是链表的时候,可想而知,for循环的效率是有多低,但是forEach底层采用的是迭代器的方式,他对数据结构是没有要求的,不管上层的数据结构是什么,他都能保证高效地执行!

因此我的最终答案:如果数据结构是ArrayList这种数据结构,那你可以采用for,但是你的数据结构如果是LinkList那你千万别再用for,应该果断采用forEach,因为数据一多起来的,for此时的效率低得可怜,说不定你的机器就瘫痪了。这也是优化的一个小技巧吧,希望能帮助大家。

并行流原理

并行流运行时:内部使用了fork-join框架,这个知识点之后学习。

并行流问题

Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8};List<Integer> listOfIntegers =new ArrayList<>(Arrays.asList(intArray));List<Integer> parallelStorage = new ArrayList<>();//List<Integer> parallelStorage = Collections.synchronizedList(new ArrayList<>());listOfIntegers.parallelStream().map(e -> {parallelStorage.add(e);return e;}).forEachOrdered(e -> System.out.print(e + " "));System.out.println();parallelStorage.stream().forEachOrdered(e -> System.out.print(e + " "));

我们并行操作是会产生由于并发导致的线程安全问题,比如上面的例子,我们并行向list中添加元素,对于parrallelStorage元素数量不固定的原因就是多线程有可能同时读取到相同的下标n同时赋值,这样就会出现元素缺失的问题了。

Part5

我们为什么需要映射?

因为在很多时候,将一个流的元素映射到另一个流对我们是非常有帮助的。比如有一个包含有名字,手机号码和钱的数据库构成的流,可能你只想要映射钱这个字段到另一个流,这时候可能之前学到的知识就还不能解决,于是映射就站了出来了。

另外,如果你希望对流中的元素应用一些转换,然后把转换的元素映射到一个新流里面,这时候也可以用映射。

我们先来看看流API库给我们提供了什么样的支持,我和大家分析一个最具有一般性的映射方法map(),相信大家就能举一反三了,map()定义如下:

Stream map(Function mapper)

其中,R指定新流的元素类型,T指定调用流的元素类型,mapper是完成映射的Function实例,被称为映射函数,映射函数必须是无状态和不干预的(大家对这二个约束条件应该很熟悉了吧)。因为map()方法会返回一个新流,因此它是一个中间操作。

Function是 java.util.function包中声明的一个函数式接口,声明如下:

@FunctionalInterface
public interface Function <T, R> {R apply(T t);
}

在map()的使有过程中,T是调用流的元素类型,R是映射的结果类型。其中,apply(T t)中的t是对被映射对象的引用,被返回映射结果。下面我们将上一篇中的例子进行变形,用映射来完成他:

假设List里面有三个Integer类型的元素分别为1,2,3。现在的需求是分别让List里面的每个元素都放大两倍后,再求积。这个需求的正确答案应该是48;

        List<Integer> lists = new ArrayList<>();lists.add(1);lists.add(2);lists.add(3);//使用并行流来处理Integer products = lists.parallelStream().reduce(1, (a,b) -> a* (b * 2), (a,b) -> a * b);System.out.println("products:" + products);//使用映射来处理Integer productMap = lists.parallelStream().map((a) -> a * 2).reduce(1, (a,b) -> a* b);System.out.println("productMap:" + productMap);

与使用并行流不同,在使用映射处理的时候,元素扩大2倍发生时机不一样了,使用并行流元素扩大是在缩减的过程当中的,而使用映射处理时,元素扩大是发生在映射过程中的。因此映射过程完程之后,不需要reduce()提供合并器了。

上面的这个例子还是简单了一点,下面再举一个例子,王者荣耀团队经济计算:

public class HeroPlayerGold {/** 使用的英雄名字 */private String hero;/** 玩家的ID */private String player;/** 获得的金币数 */private int gold;public HeroPlayerGold(String hero, String player, int gold) {this.hero = hero;this.player = player;this.gold = gold;}//省略get/set/toString
}class Gold{/** 获得的金币数 */private int gold;public Gold(int gold) {this.gold = gold;}//省略get/set/toString
}class Main{public static void main(String[] args) {List<HeroPlayerGold> lists = new ArrayList();lists.add(new HeroPlayerGold("盖伦", "RNG-Letme", 100));lists.add(new HeroPlayerGold("诸葛亮", "RNG-Xiaohu", 300));lists.add(new HeroPlayerGold("露娜", "RNG-MLXG", 300));lists.add(new HeroPlayerGold("狄仁杰", "RNG-UZI", 500));lists.add(new HeroPlayerGold("牛头", "RNG-Ming", 500));//计算团队经济OptionalInt teamMoney = lists.stream().map(hero -> new Gold(hero.getGold())).mapToInt(Gold::getGold).reduce((a, b) -> a + b);teamMoney.ifPresent(money -> System.out.println(money));//计算团队经济2OptionalInt teamMoney2 = lists.stream().mapToInt(HeroPlayerGold::getGold).reduce((a, b) -> a + b);teamMoney2.ifPresent(money -> System.out.println(money));}
}

代码应该不难理解,通过代码,大家应该知道我们假设的场景了。我们的RNG去参加王者荣耀比赛了,像这种团队游戏,观众在经济方面关注更多的可能是团队经济,而不是个人经济。

在我们 HeroPlayerGold类里面存有明星玩家,使用的英雄,和这局比赛某个玩家当前获得的金币数,我们另有一个专们管理金币的 Gold类,我们第一种计算团队经济的方式,使把 HeroPlayerGold里面的 gold字段转换到 Gold里面了 ,这里产生的新流只包含了原始流中选定的 gold字段,因为我们的原始流中包含了 hero、 player、 gold,三个字段,我们只选取了 gold字段(因为我们只关心这个字段),所以其它的两个字段被丢弃了。然后从新流取出 Gold里面的 gold字段并把他转成一个 IntStream,然后我们就要以通过缩减操作完成我们的团队经济计算了。

第一种方式,大家需要好好理解,理解了,我相信你们的项目中,很多很多地方可以用得上了,再也不需要动不动就查数据库了,怎样效率高怎样来,只是一种建议。第二种只是快速计算团队经济而已,没什么值得讲的。

接下来讲一下他的扩展方向:大家还记得我在第二篇中介绍中间操作概念的时候吗? 中间操作会产生另一个流。因此中间操作可以用来创建执行一系列动作的管道。我们可以把多个中间操作放到管道中,所以我们很容易就创建出很强大的组合操作了,发挥你的想象,打出你们的组合拳;

我现在举一个例子:比如现在相统计团队里面两个C位的经济占了多少,代码看起来可能就是这样了:

        //计算两个C位的经济和lists.stream().filter(player -> "RNG-Xiaohu".equals(player.getPlayer()) || "RNG-UZI".equals(player.getPlayer())).map(hero -> new Gold(hero.getGold())).mapToInt(Gold::getGold).reduce((a, b) -> a + b);

大家有没有感觉,这种操作怎么带有点数据库的风格啊?其实在创建数据库查询的时候,这种过滤操作十分常见,如果你经常在你的项目中使用流API,这几个条件算什么?等你们把流API用熟了之后,你们完全可以通过这种链式操作创建出非常复杂的查询,合并和选择的操作。

Stream flatMap(Function> mapper)

通过前面的学习我们知道 mapper是一个映射函数,它和map()方法也一样也会返回一个新流,我们把返回的新流称为映射流。我们提供的映射函数会处理原始流中的每一个元素,而映射流中包含了所有经过我们映射函数处理后产生的新元素。 加粗部份需要重点理解。

flatMap()操作能把原始流中的元素进行一对多的转换,并且将新生成的元素全都合并到它返回的流里面。 根据我们所学的知识,他的这种一对多的转换功能肯定就是映射函数提供的,这一点没有疑问吧!然后源码的注释上面还提供了一个例子,通过注释加例子,我相信大家都能非常清楚地理解flatMap()了。

如果orders是一批采购订单对应的流,并且每一个采购订单都包含一系列的采购项,那么 orders.flatMap(order->order.getLineItems().stream())…生成的新流将包含这一批采购订单中所有采购项。

我们用伪代码来就更加清晰了 Stream<Orders>====>Stream。大家能理解了吗?还没理解?再来一个例子:

        //(广州  深圳  上海  北京)的全拼的一些组合,下面我们就把每一个城市都划分一下List<String> citys = Arrays.asList("GuangZhou ShangHai", "GuangZhou ShenZhen", "ShangHai ShenZhen", "BeiJing ShangHai","GuangZhou BeiJing", "ShenZhen BeiJing");//这里打印的数组对应的地址citys.stream().map(mCitys -> Arrays.stream(mCitys.split(" "))).forEach(System.out::println);System.out.println();//流里面的元素还是一个数组citys.stream().map(mCitys -> Arrays.stream(mCitys.split(" "))).forEach(cities -> cities.forEach(city -> System.out.println(city + " ")));System.out.println();//直接一个flatMap()就把数组合并到映射流里面了citys.stream().flatMap(mCitys -> Arrays.stream(mCitys.split(" "))).forEach(System.out::println);System.out.println();//使用distinct()方法去重!citys.stream().flatMap(mCitys -> Arrays.stream(mCitys.split(" "))).distinct().forEach(System.out::println);

到这里,应该就能理解如果orders是一批采购订单对应的流,并且每一个采购订单都包含一系列的采购项,那么 orders.flatMap(order->order.getLineItems().stream())…生成的新流将包含这一批采购订单中所有采购项。 了吧。最后是一个去重的方法。

小结一下

通过这一篇文章,相信大家对流API中的映射已经不再陌生了,其实最需要注意的一个点是,map()和flatMap()的区别,我也一步步地带着大家理解和应用了。其实在流API这一块中,大家单单掌握概念是没什么用的,一定要去实战了,一个项目里面,集合框架这种东西用得还是特别多的,用到集合框架的大部份情况,其实都可以考虑一下用Stream流去操作一下,不仅增加效率,还可以增加业务流程的清晰度。

Part 6

我们前面的五篇文章基本都是在说将一个集合转成一个流,然后对流进行操作,其实这种操作是最多的,但有时候我们也是需要从流中收集起一些元素,并以集合的方式返回,我们把这种反向操作称为收集。

如何在流中使用收集功能?

我们先看一看流API给我们提供的方法:

R collect(Collector collector)

Collectors类是一个最终类,里面提供了大量的静态的收集器方法,借助他,我们基本可以实现各种复杂的功能了。其中 Collectors#toList()返回的收集器可以把流中元素收集到一个List中, Collectors#toSet()返回的收集器可以把流中的元素收集到一个Set中。比如:如果你想把元素收集到List中,你可以这样用, steam.collect(Collectors.toList)。

看到这里,大家有感受到流API的威力了吗?提示一下,封装一个工具类,然后结合一FastJson这种东西一起使用!是真的好用啊! 其实将数据从集合移到流中,或者将数据从流移回集合的能力,是流API给我们提供的一个强大特性,因为这允许通过流来操作集合,然后把流重新打包成集合。此外,条件合适的时候,让流操作并行发生,提高效率。

小结一下

本篇带大家入门了Stream的收集操作,但是有了些这入门操作,但我们绝大多数都这么去使用。

另外一个点,大家一定不要忘记了Collectors这个最终类,里面已经提供了很多很强大的静态方法,如果你们遇到一些特别的需求,首先要想到的应该是Collectors,如果里面的方法都不能实现你的要求,再考虑通过第二个版本的collect()方法实现你的自定义收集过程吧。

Part 7

Stream流的调试和forEach()的调试都不是特别友好,那本篇给出一个折中的调试方法,虽然不能完美解决调试的问题,但是基本上已经能解决绝大部分的调试问题了,没错,就是迭代器了,当然迭代器除了能辅助调试以外,他最重要的还是遍历功能。

先简单介绍一下传统的迭代器

迭代器是实现了Iterator接口的对象,并且Iterator接口允许遍历,获取或者移除元素。

public interface Iterator<E> {boolean hasNext();E next();default void remove() {throw new UnsupportedOperationException("remove");}default void forEachRemaining(Consumer<? super E> action) {Objects.requireNonNull(action);while (hasNext())action.accept(next());}
}

使用Iterator的一般步骤:

(1) 通过iterator()方法,获取指向集合或流开头的迭代器。

(2)建立一个hasNext()方法调用循环,只要hasNext()返回true,就继续迭代。

(3)在循环中,通过调用next()方法获取每个元素。

private static void learnIterator() {List<String> lists = Arrays.asList("A", "B", "C", "D");Iterator<String> iterator = lists.stream().iterator();while (iterator.hasNext()) {System.out.println(iterator.next());}
}

但是如果我们不修改集合的情况下,使用forEach()其实更加便利的,其实两种方式本质上面是一样的,在你编译之后,forEach()会转换成迭代器的方式进行操作了。有了迭代器,相信调试就得方便起来了,即使不能直接调试,也可以通过迭代器,反推之前,可能发生了什么。

值得注意的一点是:Java8给基本类型的流提供了额外的迭代器:PrimitiveIterator.OfInt,PrimitiveIterator.OfDouble,PrimitiveIterator.OfLong,PrimitiveIterator,但这些接口都是来扩展自Iterator接口的,所以使用上面也是相同的。

Spliterator

Spliterator是Java8新增的一种迭代器,这种迭代器由Spliterator接口定义,Spliterator也有普通的遍历元素功能,这一点与刚才说的迭代器类似的,但是,但是Spliterator方法和使用迭代器的方法是不同的。

另外,它提供的功能要比Iterator多。最终要的一点,Spliterator支持并行迭代。

public interface Spliterator<T> {boolean tryAdvance(Consumer<? super T> action);int characteristics();long estimateSize();Spliterator<T> trySplit();default void forEachRemaining(Consumer<? super T> action) {do { } while (tryAdvance(action));}
}

将Spliterator用于基本迭代任务是非常简单的,只需要调用tryAdvance()方法,直至其返回false.如果要为序列中的每个元素应用相同的动作,那么forEachRemaining()提供了一种更加高效的替代方法。

对于这两个方法,在每次迭代中将发生的动作都由Consumer对象定义的操作来决定,Consumer也是一个函数式接口,估计大家已经知道怎么分析了,这里就不带大家分析了,他的动作是指定了在迭代中下一个元素上执行的操作。下面来一个简单的例子:

private static void learnIterator() {List<String> lists = Arrays.asList("A", "B", "C", "D");Spliterator<String> spliterator = lists.stream().spliterator();while (spliterator.tryAdvance(System.out::println));
}

使用forEachRemaining()方法改进这个例子:

private static void learnIterator() {List<String> lists = Arrays.asList("A", "B", "C", "D");lists.stream().spliterator().forEachRemaining(System.out::println);
}

注意,使用这个方法时,不需要提供一个循环来一次处理一个元素,而是将各个元素作为一个整体来对待,这是Spliterator的又一个优势。

Spliterator的另一个值得注意的方法是trySplit(),它将被迭代的元素划分成了两部分,返回其中一部分的新Spliterator,另一部分则通过原来的Spliterator访问。下面再给一个简单的例子

private static void learnIterator() {List<String> lists = Arrays.asList("A", "B", "C", "D");Spliterator<String> spliterator = lists.stream().spliterator();Spliterator<String> stringSpliterator = spliterator.trySplit();if (stringSpliterator != null) stringSpliterator.forEachRemaining(System.out::println);System.out.println("------------------");spliterator.forEachRemaining(System.out::println);
}

这里只是给大家提供了这种方式而已,例子本身没有什么含义,但是当你对大数据集执行并行处理时,拆分可能是极有帮助的了。但更多情况下,要对流执行并行操作时,使用其他某个Stream方法更好,而不必手动处理Spliterator的这些细节,Spliterator最适合的场景是,给定的所有方法都不能满足你的要求时,才考虑。

最后来一个总结

到这里,Java8 Stream流的知识,基本上已经介绍完了,缩减操作,并行流,映射,还有收集是Stream流的核心内容。大家也不要忘记Collectors类,里面提供给我们的方法,基本上能处理各种各样的收集元素问题了。

java 8——Stream相关推荐

  1. 牛逼哄洪的 Java 8 Stream,性能也牛逼么?

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 Java8的Stream API可以极大提高Java程序员的生产力 ...

  2. java8 group by_java8新特性Java 8 – Stream Collectors groupingBy 示例 - Java教程

    在这篇教程中,将向你展示如何使用Java 8 Stream的Collectors,来对一个List进行分组,计算个数,求和以及排序. 1. Group By, Count and Sort 1.1 对 ...

  3. 从Java 8中的java.util.stream.Stream检索列表

    本文翻译自:Retrieving a List from a java.util.stream.Stream in Java 8 I was playing around with Java 8 la ...

  4. Java 8 Stream Api 中的 skip 和 limit 操作

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 1. 前言 Java 8 Stream API 中的sk ...

  5. Java 8 Stream API详解--转

    原文地址:http://blog.csdn.net/chszs/article/details/47038607 Java 8 Stream API详解 一.Stream API介绍 Java 8引入 ...

  6. Java 8 Stream Tutorial--转

    原文地址:http://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ This example-driven tutori ...

  7. Java 8 - Stream实战

    文章目录 Pre 练习 基础数据 (1) 找出2011年发生的所有交易,并按交易额排序(从低到高) (2) 交易员都在哪些不同的城市工作过? (3) 查找所有来自于剑桥的交易员,并按姓名排序. (4) ...

  8. Java 8 - Stream流骚操作解读2_归约操作

    文章目录 Pre 什么是归约操作 元素求和 reduce reduce如何运行的 最大值和最小值 Pre Java 8 - Stream流骚操作解读见到过的终端操作都是返回一个 boolean ( a ...

  9. Java 8 - Stream流骚操作解读

    文章目录 分类 中间操作 终端操作 使用Stream流 筛选和切片 用谓词筛选 filter 筛选各异的元素 distinct 截短流 limit 跳过元素 skip 映射 对流中每一个元素应用函数 ...

  10. Java Streams,第 1 部分: java.util.stream 库简介

    Java SE 8 中主要的新语言特性是拉姆达表达式.可以将拉姆达表达式想作一种匿名方法:像方法一样,拉姆达 表达式具有带类型的参数.主体和返回类型.但真正的亮点不是拉姆达表达式本身,而是它们所实现的 ...

最新文章

  1. CentOS中用top命令CPU负载
  2. Netronome为中国云计算大幅提速升效降成本
  3. 编程之美 set 17 拈游戏分析 (2)
  4. .Net 事件类型的实现和推荐做法
  5. 阿里云专家穆轩的《杭州九年程序员之“修炼”手册》
  6. 机器学习公开课笔记(8):k-means聚类和PCA降维
  7. centos 安装sublime3
  8. 去重查询表mysql 中数据
  9. jquery读写cookie
  10. Sipp命令行参数说明
  11. 2016中国APP分类排行榜参选入围产品公示
  12. 超赞的8款开源聊天软件
  13. jdbc常见异常及错误解决办法汇总
  14. 图文演示戴尔win10重装系统步骤
  15. 电脑无法使用typec耳机
  16. 华为优招面经(已拿offer)
  17. java 内部类声明 抽象类_Java 抽象类、接口、内部类
  18. C语言中的TRUE和FALSE
  19. USACO 2020 February Contest, Gold
  20. 加州大学伯克利分校计算机科学硕士,2020年加州大学伯克利分校排名TFE Times美国最佳计算机科学硕士专业排名第3...

热门文章

  1. Node.js 官网入门教程(二) npm(安装、包版本、卸载、npx)、package.json(scripts、devDependencies)package-lock.json(语义版本规则符号
  2. 字体包转化为ttf格式的网站分享
  3. 活用Koomail的IMAP协议功能同步Gmail邮件
  4. 第一个PWA程序-聊天室
  5. 公司网站建设的几点建议—竹晨网络
  6. java软件面试题_java软件工程师面试试题
  7. 区块链女侠杨霞:为区块链代码提供军事级的安全检测丨蚂蚁区块链大赛成都站火热报名...
  8. Unity高级功能—自发光材质的制作
  9. C语言整除、取余运算的符号问题
  10. mdp框架_SIGIR 2020 | 知识图谱上推荐推理的模仿学习框架