Stream

Stream

允许你以声明性方式处理数据集合,流还可以透明地并行处理,你就无需写任何多线程代码了。和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。

例如,以下代码会抛出一个异常,说流已经被消费掉了:

    List<String> title = Arrays.asList("Java8", "In", "Action");
    Stream<String> s = title.stream();
    s.forEach(System.out::println);
    s.forEach(System.out::println);
集合与流的区别:
  1. 集合是一个内存中的数据结构,它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。(你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分。)

    则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。

  2. 使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。 相反,Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。

    内部迭代:内部迭代时,项目可以透明地并行处理,或者用更优化的顺序进行处理,如果使用外部迭代,这些优化都是很困难的,通过写for-each而选择了外部迭代,那你基本上就要自己管理所有的并行问题了。
    外部迭代:外部迭代一个集合,显式地取出每个项目再加以处理,Streams库的内部迭代可以自动选择一种适合你硬件的数据表示和并行实现。


流操作
import static java.util.stream.Collectors.toList;
List<String> threeHighCaloricDishNames =
   menu.stream()
            .filter(d -> d.getCalories() > 300)
            .map(Dish::getName)
            .limit(3)  
            .collect(toList()); 
  1. filter:接受Lambda,从流中排除某些元素。
  2. map:—接受一个Lambda,将元素转换成其他形式或提取信息。
  3. limit:截断流,使其元素不超过给定数量
  4. collect:将流转换为其他形式。
    除了collect之外,所有这些操作都会返回另一个流,这样它们就可以接成一条流水线,于是就可以看作对源的一个查询。最后collect操作开始处理流水线,并返回结果(它和别的操作不一样,因为它返回的不是流,在这里是一个List),在调用collect之前,没有任
    何结果产生,实际上根本就没有从menu里选择元素。
  • 中间操作
    诸如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理,是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。

  • 终端操作
    终端操作会从流的流水线生成结果,其结果是任何不是流的值。在下面的流水线中,forEach是一个返回void的终端操作,它会对源中的每道菜应用一个Lambda。把System.out.println传递给forEach:

    menu.stream().forEach(System.out::println); 
    
使用流:
  1. 一个数据源(如集合)来执行一个查询;
  2. 一个中间操作链,形成一条流的流水线;
  3. 一个终端操作,执行流水线,并能生成结果。
方法名 作用
filter 该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流
distinct 它会返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流,可确保流中没有重复元素
limit(n) 方法会返回一个不超过给定长度的流
count 返回流中元素的个数
sorted 对流进行排序
skip(n) 返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流,与limit()互补
map 它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素
flatMap 让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流
  • sorted

    list.stream().sorted(Comparator.comparing(Student::getAge)) 
    list.stream().sorted(Comparator.comparing(Student::getAge).reversed())//倒序
    
  • flatMap
    例如:给定单词列表["Hello","World"],想要返回列表["H","e","l", "o","W","r","d"]。

    你可以把每个单词映射成一张字符表,然后调用distinct来过滤
    重复的字符。

    words.stream()
           .map(word -> word.split(""))
           .distinct()
           .collect(toList()); 
    

    这个方法的问题在于,传递给map方法的Lambda为每个单词返回了一个String[](String列表)。因此,map返回的流实际上是Stream<String[]>类型的。你真正想要的是用Stream<String>来表示一个字符流。

image.png

这时候,可能想到有一个叫作Arrays.stream()的方法可以接受
一个数组并产生一个流,例如:

String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords); 
    words.stream()
         .map(word -> word.split(""))
         .map(Arrays::stream)
         .distinct()
         .collect(toList());

但是这种方案依然不行,因为,你现在得到的是一个流的列表(更准确地说是Stream<String>)你先是把每个单词转换成一个字母数组,然后把每个数组变成了一个独立的流,但是这样每个数组变成一个单独的流。

使用flatMap方法:

List<String> uniqueCharacters =
     words.stream()
          .map(w -> w.split("")) //将每个单词转换为由其字母构成的数组
          .flatMap(Arrays::stream)//将各个生成流扁平化为单个流
          .distinct()
          .collect(Collectors.toList()); 

使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。

image.png

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

例如: 给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。为简单起见,你可以用有两个元素的数组来代表数对。

List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs =
      numbers1.stream()
              .flatMap(i -> numbers2.stream()
                                    .map(j -> new int[]{i, j})
                      )
              .collect(toList()); 

若只返回总和能被3整除的数对呢?例如(2, 4)和(3, 3)是可以的(filter可以配合谓词使用来筛选流中的元素。因为在flatMap操作后,你有了一个代表数对的int[]流,所以你只需要一个谓词来检查总和是否能被3整除就可以了)

List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs =
     numbers1.stream()
             .flatMap(i ->
                        numbers2.stream()
                                .filter(j -> (i + j) % 3 == 0)
                                .map(j -> new int[]{i, j})
                      )
             .collect(toList()); 
  • 检查与匹配
方法 作用
anyMatch 流中是否有一个元素能匹配给定的谓词,返回一个boolean,因此是一个终端操作
allMatch 流中的元素是否都能匹配给定的谓词
noneMatch 确保流中没有任何元素与给定的谓词匹配

anyMatch、allMatch和noneMatch这三个操作都用到了的短路,对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。

  • 查找元素
    findAny:将返回当前流中的任意元素。它可以与其他流操作结合使用。

    Optional<Dish> dish =
           menu.stream()
               .filter(Dish::isVegetarian)
               .findAny(); 
    

Optional

Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。在上面的代码中,findAny可能什么元素都没找到。这样就不用返回容易出问题的null了。

  • Optional里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法:
  1. isPresent()将在Optional包含值的时候返回true, 否则返回false。
  2. ifPresent()会在值存在的时候执行给定的代码块。
  3. T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。
  4. T orElse(T other)会在值存在时返回值,否则返回一个默认值。

查找第一个元素:
findFirst:有些流有一个出现顺序来指定流中项目出现的逻辑顺序(比如由List或排序好的数据列生成的流)。对于这种流,可能想要找到第一个元素,它的工作方式类似于findany。例如下面代码会返回第一个平方能被3整除的数

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =
     someNumbers.stream()
                .map(x -> x * x)
                .filter(x -> x % 3 == 0)
                .findFirst(); 

何时使用findFirst和findAny
为什么会同时有findFirst和findAny呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。


归约:将流中的元素反复结合起来,得到一个值的操作。
  • reduce

    int sum = numbers.stream().reduce(0, (a, b) -> a + b); 
    

reduce接收两个参数:

  1. 一个初始值,这里是0
  2. 一个BinaryOperator<T>来将两个元素结合起来产生一个新值,这里用的是lambda (a, b) -> a + b

Integer类现在有了一个静态的sum方法来对两个数求和,

    int sum = numbers.stream().reduce(0, Integer::sum); 

若无初始值
reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:

   Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b)); 

考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。

  • 最大值和最小值

用Lambda写就是:Lambda (x, y) -> x < y ? x : y
也可以写成:Optional<Integer> max = numbers.stream().reduce(Integer::max);
同样最小值:Optional<Integer> min = numbers.stream().reduce(Integer::min);

归约方法的优势与并行化

相比于逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!

数值流:

利用reduce方法计算流中元素的总和,但是这段代码的问题是,他有一个暗含的装箱成本,每个Integer都必须拆箱成原始类型,再进行求和,因此Stream API提供了原始类型流特化,专门处理支持数值流的方法。

int calories = menu.stream()
                   .map(Dish::getCalories)
                   .reduce(0, Integer::sum); 
  • 原始类型流特化

IntStreamDoubleStreamLongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。

  1. 映射到数值流

     int calories = menu.stream()//返回一个Stream<Dish>
                        .mapToInt(Dish::getCalories)//返回一个IntStream
                        .sum(); //如果流是空的,sum则默认返回0,IntStream还支持其他的方便方法,如max、min、average等
    
  2. 转回对象流
    mapToObjboxed的区别:???

     IntStream intStream = menu.stream().mapToInt(Dish::getCalories);//将Stream转换为数值流
     Stream<Integer> stream = intStream.boxed(); //将数值流转换为Stream
    
  3. 默认值OptionalInt
    由于求和时默认值为0,计算IntStream中的最大值时,如何区分没有元素的流和最大值真的是0的流呢?

    Optional同样可以用OptionalIntOptionalDoubleOptionalLong进行参数化。

    例如:找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt

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

如果没有最大值,可以显式的处理OptionalInt去定义一个默认值:

    int max = maxCalories.orElse(1);
  • 数值范围
    生成成1和100之间的所有数字,Java 8引入了两个可以用于IntStreamLongStream的静态方法,帮助生成这种范围:rangerangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。
    例如:

    IntStream evenNumbers = IntStream.rangeClosed(1, 100)
                                     .filter(n -> n % 2 == 0);
                                     .count();//从1到100有50个偶数
    
    IntStream evenNumbers = IntStream.range(1, 100)
                                     .filter(n -> n % 2 == 0);
                                     .count();//由于不包含100,结果为49
    
构建流:
  1. 由值创建流
    使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。
    例如:

     Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
     stream.map(String::toUpperCase).forEach(System.out::println);
     //也可以通过empty得到一个空流: 
     Stream<String> emptyStream = Stream.empty();  
    
  2. 由数组创建流
    使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。
    例如:

     int[] numbers = {2, 3, 5, 7, 11, 13};
     int sum = Arrays.stream(numbers).sum(); 
    
  3. 由文件生成流
    java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。
    例如:下面的方法可以查看文件中有多少各不相同的词:

     long uniqueWords = 0;
     try(Stream<String> lines =
               Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){//流会自动关闭
     uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                                               .distinct()
                                               .count();
     }
     catch(IOException e){
     }
    

    Files.lines会得到一个流,其中每个元素都是给定文件中的一行。然后可以对line调用split方法将每行拆分成单词,这里应该使用flatMap产生扁平流,否则将会给每一行生成一个单词流。然后用distinct和count方法链接起来,就可以得到有多少各不相同的单词。

  4. 由函数生成流
    Stream API提供了两个静态方法来从函数生成流:Stream.iterateStream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterategenerate产生的流会用给定的函数按需创建值,一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。

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

    iterate方法接受一个初始值(在这是0),还有一个依次应用在每个产生的新值上的Lambda。这里使用Lambda n -> n + 2,返回的是前一个元素加上2。,此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。使用limit方法限制流的大小。

    • 生成

       Stream.generate(Math::random)
             .limit(5)
             .forEach(System.out::println); 
      

    iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。在这里使用的供应源(指向Math.random的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用但是在并行代码中使用有状态的供应源是不安全的

有状态和无状态操作

filtermap等操作是无状态的,它们并不存储任何状态。reduce等操作要存储状态才能计算出一个值。sorteddistinct等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。

----摘自《java8实战》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 一、前言   在目前用到的JDK8的功能当中,毫无疑问Stream的使用是最多的,所以通过这篇文章来学习总结一下。...
    骑着乌龟去看海阅读 2,113评论 1 7
  • 1、Stream简介 Stream作为Java 8的一大亮点,是对集合(Collection)、数组对象功能的增强...
    Albert_Yu阅读 6,763评论 1 21
  • Stream API Stream是Java8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,但是将执...
    sprainkle阅读 2,227评论 0 3
  • Jav8中,在核心类库中引入了新的概念,流(Stream)。流使得程序媛们得以站在更高的抽象层次上对集合进行操作。...
    仁昌居士阅读 3,619评论 0 6
  • 天下没有不散的宴席,道不同不相为谋!我不会去强求改变每个人的观点,我也觉得我自己的做法没错,我不会去刻意的迎合谁,...
    海绵_a0fe阅读 96评论 0 0