Java8 -使用流(2)

1.数值流

  我们在前面看到了可以使用reduce方法计算流中元素的总和。例如,你可以像下面这样计算菜单的热量:

int calories = 
        menu.stream()
       .map(Dish::getCalories)
       .reduce(0, Integer::sum);

  这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始数据类型,再进行求和。要是可以直接像下面调用sum方法就好了:

 int calories = 
      menu.stream()
      .map(Dish::getCalories)
     .sum();

  但是这是不可能的。问题在于map方法会生成一个Stream<T>。虽然流中的元素是Integer类型,但是Streams接口没有定义sum方法。为什么没有呢?比方说,你只有一个向menu那样的Stream<Dish>,把各种菜加起来是没有任何意义。但不要担心,Stream API还提供了原始类型流特化,专门支持处理数值流的方法。

(1).原始类型特化

  Java8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化称为int、long和double。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把他们转换回对象的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性--即类似int和Integer之间的效率差异。

A.映射到数值流

  将流转换为特换版本的常用是mapToInt、mapToDouble和mapToLong。这些方法和前面说的map方法的工作方式一样,只是他们返回的是一个特化流,而不是Stream<T>。例如,你可以像下面这样用mapToInt对menu中的卡路里总和:

int calories = 
       menu.stream()  //返回一个Stream<Dish>
      .mapToInt(Dish::getCalories) //返回一个IntStream
      .sum();

  这里,mapToInt会从每道菜中提取热量(用一个Integer表示),并且返回一个IntStream(而不是Stream<Integer>)。然后你就可以调用IntStream接口定义的sum方法,对卡路里求和了!请注意,如果流时空的,sum默认返回0.IntStream还支持其他的方便方法,如max、min。average等。

B.转换回对象流

  同样,一旦有了数值流,你可能会想把转换回非特化流。例如IntStream上的操作只能产生原始整数:Instream的map操作接收的Lambda必须接收int并且返回int(一个IntUnaryOperator)。但是你可能想要生成另一类值,比如Dish值。为此,你需要访问Stream接口中定义的那些更加广义的操作。要把原始流转换为一般流(每个int都会装箱成一个Integer),可以使用boxed方法,如下所示:

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);// 将Stream转换为数值流
Stream<Integer> boxed = intStream.boxed(); //将数值流转换为Stream
C.默认值OptionalInt

  求和的那个例子很容易,因为它有一个默认值:0。但是,如果你要计算IntStream中的最大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢?前面我们介绍了Optional类,这是一个可以表示值存在或者不存在的容器。Optional可以用Integer、String等参数类型来参数化。对于三种原始流特化,也分别有一个Optional原始数据类型特化版本:OptionalInt、OptionalDouble和OptionalLong。
  例如:要找到IntStream中的最大元素,可以调用方法,它会返回一个OptionalInt:

OptionalInt maxCalories = 
      menu.stream()
      .mapToInt(Dish::getCalories)
      .max();

  现在,如果没有最大值的话,你就可以显示处理OptionalInt去定义一个默认值了:

int max = maxCalories.orElse(0); //如果没有最大值的话,显示提供一个默认最大值

(2).数值范围

  和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成1到100之间的所有数字。Java8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接收起始值,第二个参数接收接收结束值。但是rang是不包含结束值的,而rangeClosed则包含结束值。让我们来看一个例子:

IntStream eventNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2== 0); //一个从1到100的偶数流
 System.out.println(eventNumbers.count());  //从1到100一共有50个偶数

  这里我们用了rangeClosed方法来生成1到100之间的所有数字。它会产生一个流,然后你可以链接filter方法,只选出偶数。到目前为止还没有进行任何计算。最后,你对生成的流调用count。因为count是一个终端操作,所以处理流,并且返回结果50,这正是1到100(包括两端)中所有偶数的个数。请注意,比较一下,如果改用IntStream.rang(1, 100),则结果将会是49个偶数,因为range是不包含结束值的。

2.构建流

  到现在,我们已经知道流对于表达数据处理查询是非常的强大而且有用的。到目前为止,你已经可以能够使用stream方法将集合变成流了。此外,我们还介绍了如何根据竖直范围创建数值流。但是创建流的方法还有好多!

(1).由值创建流

  你可以使用静态方法Stream.of,通过显示地创建一个流。它可以接收任意数量的参数。例如,一下代码直接使用Stream.of创建了一个字符流。然后,你可以讲字符串转换为大小,再一个个打印出来:

Stream<String> stream = Stream.of("Java 8", "Android", "Lambda");
stream.map(String::toUpperCase).forEach(System.out::println);

  你可以使用empty得到一个空流,如下所示:

Stream<String> emptyStream = Stream.empty();

(2).由数组创建流

  你可以使用静态方法Arrays.stream从数组创建一个流。它接收一个数组作为参数。例如,你可以使用讲一个原始类型int的数组转换成一个IntStream,如下所示:

int[] numbers = { 2, 3, 5, 7, 11, 13 };
int sum = Arrays.stream(numbers).sum();

(3).由文件生成流

  Java中用于处理文件等I/O操作的NIO API(非阻塞I/O)已更新,以便利用Stream API。java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是File.lines,它会返回一个由指定文件中的各行构成的字符串流。使用你迄今学到的内容,你可以用这个方法看看这个文件有多少个不相同的词:

long uniqueWords = 0;
        try (Stream<String> lines = Files.lines(Paths.get("D://a.txt"), Charset.defaultCharset())) { //流会自动关闭
            uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))) //生成单词流
                               .distinct()  //删除重复项
                               .count();   //数一数有多少各不相同的单词
        } catch (IOException e) {
            //如果打开文件是出现异常则加以处理
        }

  你可以使用Files.lines得到一个流,其中的每个元素都是给定文件中的一行。然后,你可以对line调用splie方法将行拆分成一个单词。应该注意的是,你该如何利用flatMap产生一个扁平的单词流,而不是给每一行生成一个单词流对象。最后,把distinct和count方法链接起来,数一数流有多少各不相同的单词。

(4).由函数生成流:创建无限流

  Stream APi 提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像固定集合创建的流那样有固定大小的流。有iterate和generate产生的流会用给定的函数按需要创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n) 来对这种流加以限制,以避免打印无穷个个值。

A.迭代

  我们先来看一个iterate的简单例子,然后解释:

Stream.iterate(0,  n -> n + 2)
        .limit(10)
        .forEach(System.out::println);

  iterate方法接收一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator<T>类型)。这里,我们使用Lambda表达式 n -> n+2,返回的是前一个元素加上2.因此,iterate方法生成了一个所有正偶数的流:流的第一个元素是初始值0。然后加上2来生成新的值2,再加上2来得到新的值4,,以此类推。这种iterate操作基本上是顺序的,因为结果取决于前一次的结果。请注意,此操作将生成一个无限流--这个流是无界的。正如我们前面所讨论的,这是流和集合之间的关键区别。我们使用limit方法来显示限制流的大小。这里只选择了前10个偶数。然后可以调用forEach终端操作来消费流,并且分别打印每个元素。
   一般来说,在需要依次生成一系列的值的时候应该使用iterate方法,比如一系列日期:1月31日,2月1日,以此类推。

B.生成

  iterate方法类似,generate方法也可以让你按需生成一个无限流。但是generate不是依次对每个新生成的值应用函数的。它接收一个Supplier<T>类型的Lambda提供新的值。我们先来看一个简单的用法:

Stream.generate(() -> (int)(Math.random() * 63))
               .distinct()
               .limit(63)
               .sorted(Integer::compareTo)
               .forEach(System.out::println);

  这段代码将生流,其中63个0到63之间的随机数。
  Math.random静态方法被用作新值生成器。同样limit方法显示限制流的大小,否则流会无限长。
  你可能会想到,generate方法还有什么用途。我们使用的供应源(指向Math.random的Lambda表达式)是无状态的:它不会在任何地方记录新值,以备以后计算使用。但是供应源不一定是无状态的。你可以创建存储状态的供应源,你可以修改状态,并在为流生成下一个值时使用。举个例子,我们将如何利用generate创建斐波那契数列,这样就可以利用iterate方法的办法比较一下。但是很重要的一点是,在并行代码中使用有状态的供应源是不安全的。因此下面的代码仅仅是为了内容完整,应尽量避免使用。
  我们在这个例子中会使用IntSrream说明避免装箱操作的代码。IntStream的generate方法会接收一个IntSupplier,而不是Supplier<T>。例如,可以这样来生成一个全是1的无限流:

IntStream ones = IntStream.generate(() -> 1);

  之前,我们已经知道,Lambda允许你创建函数式接口的实例,只要直接内联提供方法的实现就可以。你也可以像下面这样,通过实现IntSupplier接扣中定义的getAsInt方法显示传递一个对象(虽然这看起来是无缘无故的圈子,也请你耐心的看):

IntStream tows = IntStream.generate(new IntSupplier() {
            
            @Override
            public int getAsInt() {
                return 2;
            }
        });

  generate方法将利用给定供应源,并且反复的调用getAsInt方法,而这个方法总是返回2。但是这里使用的是匿名类和Lambda的区别在于,匿名类可以通过字段定义状态,而且状态又可以用getAsInt方法来修改。这是副作用的例子。你迄今见过的所有Lambda都是没有副作用的;它们没有改变任何状态。
  回到斐波那契数列的任务上,你现在需要做的是建立一个IntSupplier,它要把前一项的值保存在状态中,以便getAsInt用它来计算下一项。此外,在下一次调用它的时候,还要更新IntSupplier的状态。下面的代码就是如何创建一个调用时返回下一个斐波那契的IntSupplier:

IntSupplier fib  = new IntSupplier() {
            private int previous = 0;
            private int current = 1;
            @Override
            public int getAsInt() {
                int oldPrevious = this.previous;
                int nextValue = this.previous + this.current;
                this.previous = this.current;
                this.current = nextValue;
                return oldPrevious;
            }
        };
        IntStream.generate(fib).limit(10).forEach(System.out::println);

  前面的代码创建了一个IntSupplier的实例。此对象有可变的对象:它在两个实例变量中记录了前一个斐波那契和当前的斐波那契向。getAsInt方法在调用时会改变对象的状态,由此在每次调用时产生新的值。相比之下,使用iterate方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元祖。你在之后了解到,你应该采用始终不变的方法,以便并行处理流,并且保持结果正确。请注意,因为你处理的是一个无限流,所以必须使用limit方法来限制它的大小;否恩泽,终端操作(这里是forEach)将永远计算下去。同样,你不能对无限流做排序或者归约,因为所有的元素都需要处理,而且这永远也完成不了的!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Java8 in action 没有共享的可变数据,将方法和函数即代码传递给其他方法的能力就是我们平常所说的函数式...
    铁牛很铁阅读 1,208评论 1 2
  • 第一章 为什么要关心Java 8 使用Stream库来选择最佳低级执行机制可以避免使用Synchronized(同...
    谢随安阅读 1,481评论 0 4
  • 概要 流让你从外部迭代转向内部迭代。这样,你就用不着写下面这样的代码来显式地管理数据集合的迭代(外部迭代)了: 现...
    浔它芉咟渡阅读 1,483评论 1 2
  • Jav8中,在核心类库中引入了新的概念,流(Stream)。流使得程序媛们得以站在更高的抽象层次上对集合进行操作。...
    仁昌居士阅读 3,619评论 0 6