Java8新特性之Stream流 刺骨的言语ヽ痛彻心扉 2022-05-16 11:36 222阅读 0赞 #### 什么是Stream #### Stream是Java8中处理集合的关键抽象概念,他可以指定你希望对集合进行非常复杂的查找,过滤和映射数据等操作。Steam是一种高效的处理数据的一种方式。 备注: Java中的集合讲的是数据,而Stream流讲究的是对数据的处理 注意:1. Stream自己不会存储元素。 2. Streatm不会改变原对象。相反,他们会返回一个持有结果的新Stream。 3. Stream操作时延迟执行,也就是他们会等到需要结果的时候才会执行。 #### Stream流的构成 #### > 当我们使用Stream流的时候,通常包括三个基本步骤: > > 获取一个数据源(source)----> 数据转换 -----> 执行操作获取想要的结果, 每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。 > > ![stream流的流程图][stream] > > 第一步:创建Stream 一个数据源(如:集合或数组),获取一个流。 > > 第二步: 中间操作 一个中间操作链,对数据源中的数据进行处理操作。 > > 第三步:终止操作 执行中间操作链,并返回产生的最终结果。 #### 创建Stream流的方式 #### > 第一种:通过Collection和数组 > > 1. Collection.stream(); > 2. Collection.parallelStream(); > 3. Arrays.stream(T array) or Stream.of() > 第二种:从BufferedReader中获取 > > 1. Java.io.BufferedReader.lines(); > 第三种:静态工厂 > > 1. java.util.stream.IntStream.range(); > 2. java.nio.file.Files.walk(); > 第四种: 自己构建 > > 1. java.util.Spliterator > > 第五种: strem创建无限流 > > 1. public static< T> Stream< T> iterate(final T seed, final UnaryOperator< T> f); > 2. public static< T> Stream< T> generate(Supplier< T> s); > 第六种: 其他 > > 1. Random.ints(); > 2. BitSet.stream(); > 3. Pattern.splitAsStream(java.lang.CharSequence); > 4. JarFile.stream(); #### Stream流的中间操作和终止操作 #### > 流的操作类型总共可以分为两种: > > * Intermediate(中间操作):一个流可以后台跟随零个或多个intermediate操作。其目的主要是打开流,做出对应的数据映射/过滤,而后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(Lazy),也就是说调用到这类方法,并没有真正开始流的遍历。 > * Terminal(终止操作): 一个流只能有一个terminal操作,当操作执行这个操作后,此时的流就被使用“光”了,无法再被操作。也就是流的最后一部操作。Terminal操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个side effect. > * short-circuiting(短循环) > > 还有一个操作被称为`short-circuiting`,用以指: > > * 对于一个intermediate操作,如果他接受的是一个无限大(infinite/unbounded)的Stream,但返回一个有限的新Stream > * 对于一个Terminal操作,如果他接受的是要给无限大的Stream,单能在有限的时间计算出结果。 > * 总结:当操作一个无限大的Streamm,此时有希望再有限的时间内进行完成,此时可以再管道内拥有一个short-circuiting操作可以增加处理的效率。 > > 在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。 > 当把一个数据包装成Stream后,就要开始对里卖弄的元素进行各种操作了。常见的中间操作和终止操纵的方法以及短循环可以归类如下。 > > * Intermediate: map(mapToInt,flatMap等),filter(),distinct(),sorted(),peek(),limit(),skip(),paraller(),sequential(),unordered() > * Terminal: forEach(),forEachOrdered(),toArray(),reduce(),collect(),min(),max(),count(),anyMatch(),allMatch(),noneMatch(),findFirst(),findAny(),iterator() > * Short-circuiting: anyMatch(),allMatch(),noneMatch(),findFirst(),findAny(),limit() #### Stream流的操作方法 #### ###### map/flatMap : 把input stream中的每一个元素,映射成output stream的另外一个元素 ###### > * map : 一对一操作 , 每个输入元素都按照规则转换为另外一个元素。 > > 1. 讲单词转换为大写 > List<String> list = Arrays.asList("hello", "world"); > List<String> collect = list.stream().map(String::toUpperCase).collect(Collectors.toList()); > // 输出结果: HELLO WORLD > > 2. 求平方数 > List<Integer> nums = Arrays.asList(1, 2, 3, 4); > List<Integer> squareNums = nums.stream(). > map(n -> n * n). > collect(Collectors.toList()); > //输出结果: 1 4 9 16 > > * flatMap: 一对多操作,flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。 > flatMap A-> B属性(是个集合),最终得到所有的A元素里卖弄的所有B属性集合。 1. 一对多 Stream<List<Integer>> inputStream = Stream.of( Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6) ); Stream<Integer> outputStream = inputStream. flatMap((childList) -> childList.stream()); //输出结果: 1 2 3 4 5 6 ###### filter: filter对原始stream进行某项测试,通过测试的元素都被留下来生成一个新的Stream. ###### 1. 留下偶数操作 Integer[] sixNums = {1, 2, 3, 4, 5, 6}; Integer[] evens = Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new); //输出结果: 2 4 6 2. 把单词挑出来 BufferedReader reader = new BufferedReader(new StringReader("haha")); List<String> output = reader.lines(). flatMap(line -> Stream.of(line.split("h"))). filter(word -> word.length() > 0). collect(Collectors.toList()); Stream.of(output).forEach(System.out::println); //输出结果: [a,a] 备注: 首先该方法会把每行的单词用flatMap整理到新的Stream,然后保留长度不为0的,就是当前文章中的所有单词。 ###### **forEach** : forEach方法是一个Terminal操作,接受一个Lambda表达式,然后再Stream的每一个元素上执行该表达式。 ###### 1.打印姓名(forEach 和 pre-java8 的对比) // Java 8 roster.stream() .filter(p -> p.getGender() == Person.Sex.MALE) .forEach(p -> System.out.println(p.getName())); // Pre-Java 8 for (Person p : roster) { if (p.getGender() == Person.Sex.MALE) { System.out.println(p.getName()); } } > 对一个人员集合遍历,找出男性并打印姓名。可以看出来,forEach 是为 Lambda 而设计的,保持了最紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。当需要为多核系统优化时,可以 parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时 forEach 本身的实现不需要调整,而 Java8 以前的 for 循环 code 可能需要加入额外的多线程逻辑。 > > 但一般认为,forEach 和常规 for 循环的差异不涉及到性能,它们仅仅是函数式风格与传统 Java 风格的差别。 > > 另外一点需要注意,forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面的代码是错误的: ream.forEach(element -> doOneThing(element)); stream.forEach(element -> doAnotherThing(element)); > 相反,具有相似功能的 intermediate 操作 `peek` 可以达到上述目的。如下是出现在该 api javadoc 上的一个示例。 1. peek 对每个元素执行操作并返回一个新的 Stream Stream.of("one", "two", "three", "four") .filter(e -> e.length() > 3) .peek(e -> System.out.println("Filtered value: " + e)) .map(String::toUpperCase) .peek(e -> System.out.println("Mapped value: " + e)) .collect(Collectors.toList()); > forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。 ###### **findFirst** : 这是一个 termimal 兼 short-circuiting 操作,它总是返回 Stream 的第一个元素,或者空。 ###### > 注意点: 这里的返回值类型是Optional.这也是模仿Scala语言中的概念,作为一个容器,他可能含有某值,或者不包含。使用他的目的是尽可能避免NullpointerException. > > Optional的两个用例展示 String strA = " abcd ", strB = null; print(strA); print(""); print(strB); getLength(strA); getLength(""); getLength(strB); public static void print(String text) { // Java 8的函数式编程Optional Optional.ofNullable(text).ifPresent(System.out::println); // Pre-Java 8 if (text != null) { System.out.println(text); } } public static int getLength(String text) { // Java 8 return Optional.ofNullable(text).map(String::length).orElse(-1); // Pre-Java 8 // return if (text != null) ? text.length() : -1; }; > 在更复杂的 if (xx != null) 的情况中,使用 Optional 代码的可读性更好,而且它提供的是编译时检查,能极大的降低 NPE 这种 Runtime Exception 对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。 > > Stream 中的 findAny、max/min、reduce 等方法等返回 Optional 值。还有例如 IntStream.average() 返回 OptionalDouble 等等。 ###### reduce: 把stream元素组合起来,他提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面stream的第一个,第二个,第n个元素组合。例如字符串拼接,数值的sum,min,max,average都是特殊的reduce ###### > Integer sum = integers.reduce(0,(a,b)->a+b);或 > > Integer sum = integers.reduce(0,Integer::sum); > > 也有没有起始值的情况,这是会把stream的前面两个元素组合起来,返回的是optional > > reduce案例展示 // 字符串连接,concat = "ABCD" String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); // 求最小值,minValue = -3.0 double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); // 求和,sumValue = 10, 有起始值 int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum); // 求和,sumValue = 10, 无起始值 sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get(); // 过滤,字符串连接,concat = "ace" concat = Stream.of("a", "B", "c", "D", "e", "F"). filter(x -> x.compareTo("Z") > 0). reduce("", String::concat); ###### limit/skip: limit返回Stream的前面几个元素;Skip则是扔掉前n个元素(它是一个叫subStream的方法名改而来) ###### > limit和skip案例展示 public void testLimitAndSkip() { List<Person> persons = new ArrayList(); for (int i = 1; i <= 10000; i++) { Person person = new Person(i, "name" + i); persons.add(person); } List<String> personList2 = persons.stream(). map(Person::getName).limit(10).skip(3).collect(Collectors.toList()); System.out.println(personList2); } private class Person { public int no; private String name; public Person (int no, String name) { this.no = no; this.name = name; } public String getName() { System.out.println(name); return name; } } //打印结果: [name4, name5, name6, name7, name8, name9, name10] 备注:skip把前面的三个都跳过 > 注意点: limit和skip对sorted后的运行测试无影响,因为他们两个方法都属于terminal方法 List<Person> persons = new ArrayList(); for (int i = 1; i <= 5; i++) { Person person = new Person(i, "name" + i); persons.add(person); } List<Person> personList2 = persons.stream().sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).limit(2).collect(Collectors.toList()); System.out.println(personList2); // 打印结果: name2 name1 name3 name2 name4 name3 name5 name4 [Person{no=1, name='name1'}, Person{no=2, name='name2'}] 即虽然最后的返回元素数量是 2,但整个管道中的 sorted 表达式执行次数没有像前面例子相应减少。 > 注意点: 对一个parallel的Stream流来说,如果其元素是有序的,那么limit操作的成本会比较大,应为他的返回对象必须是前n个也有一个次序的元素。取而代之的策略是取元素间的次序,或者不要使用parallel stream。 ###### **sorted** :对Stream中的元素进行排序操作,他比数组的排序更强之处在于可以首先对Stream进行各类map,filter,limit,skip甚至是distctl来减少元素数量后,再进行排序,从而提高效率 ###### > sorted案例:再排序前进行limit或skip操作 List<Person> persons = new ArrayList(); for (int i = 1; i <= 5; i++) { Person person = new Person(i, "name" + i); persons.add(person); } List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList()); System.out.println(personList2); //打印结果: name2 name1 [Person{no=1, name='name1'}, Person{no=2, name='name2'}] ###### **min/max/distinct** :min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n)。同时它们作为特殊的 reduce 方法被独立出来也是因为求最大最小值是很常见的操作。 ###### > 案例: 找出最长一行的长度 BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log")); int longest = br.lines().mapToInt(String::length).max().getAsInt(); br.close(); System.out.println(longest); > 案例: 找出全文的单词,转小写并排序 List<String> words = br.lines().flatMap(line -> Stream.of(line.split(" "))).filter(word -> word.length() > 0).map(String::toLowerCase).distinct().sorted().collect(Collectors.toList()); br.close(); System.out.println(words); ###### **Match** : 匹配操作 ###### > Stream有三个match方法: > > * allMatch: Stream中全部元素符合传入的predicate,返回true > * anyMatch:Stream中只要有一个元素符合传入的predicate,返回true > * noneMatch:Stream中没有一个元素符合传入的predicate,返回true > ##### 使用 Match案例: ##### List<Person> persons = new ArrayList(); persons.add(new Person(1, "name" + 1, 10)); persons.add(new Person(2, "name" + 2, 21)); persons.add(new Person(3, "name" + 3, 34)); persons.add(new Person(4, "name" + 4, 6)); persons.add(new Person(5, "name" + 5, 55)); boolean isAllAdult = persons.stream(). allMatch(p -> p.getAge() > 18); System.out.println("All are adult? " + isAllAdult); boolean isThereAnyChild = persons.stream(). anyMatch(p -> p.getAge() < 12); System.out.println("Any child? " + isThereAnyChild); //打印结果: All are adult? false Any child? true #### Stream进阶: 自己生成流 #### ###### Stream.generate: 通过实现Supplier接口,你可以自己来控制流的生成。 ###### > 应用场景: 通常用于随机数,常量的stream.或者需要前后元素见维持着某种状态的Stream.把Supplier实例传递给Stream.generate()生成的Stream.默认是串行(相对于parallel而言)但是无序的(相对于ordered)而言。由于他是无限的,再管道中,必须利用limit之类的操作显示Stream大小。 > 案例: 生成10个随机整数 Random seed = new Random(); Supplier<Integer> random = seed::nextInt; Stream.generate(random).limit(10).forEach(System.out::println); //Another way IntStream.generate(() -> (int) (System.nanoTime() % 100)). limit(10).forEach(System.out::println); > Stream.generator()还接受自己实现的Supplier.例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算Stream的每个元素值。这些都是维持状态。 > > 案例: 自实现Supplier Stream.generate(new PersonSupplier()). limit(10). forEach(p -> System.out.println(p.getName() + ", " + p.getAge())); private class PersonSupplier implements Supplier<Person> { private int index = 0; private Random random = new Random(); @Override public Person get() { return new Person(index++, "StormTestUser" + index, random.nextInt(100)); } } //打印结果: StormTestUser1, 9 StormTestUser2, 12 StormTestUser3, 88 StormTestUser4, 51 StormTestUser5, 22 StormTestUser6, 28 StormTestUser7, 81 StormTestUser8, 51 StormTestUser9, 4 StormTestUser10, 76 ###### Stream.iterator : iterator和reduce操作很像,接受一个种子值,和一个UnaryOperator(例如f).然后种子值成为Stream的第一个元素,f(seed)为第二个,f(f(seed))第三个,以次内推。 ###### > 案例: 生成一个等差数列 Stream.iterator(0,n->n+3).limit(10).forEach(x-> System.out.println(x+" ")); //打印结果: 0 3 6 9 12 15 18 21 24 27 > 与 Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。 #### Stream进阶: 用Collectors进行reduction操作 #### > java.util.stream.Collectors主要作用就是辅助进行各类有用的reduction操作,例如转变输出为Collectior,把Stream元素进行归组。 > > Collectors.groupingBy() Collections.partitioningBy() > > 案例: 按照年龄归组 Collectors.groupingBy方法 // 首先生成100人的信息,然后按照年龄归组,相同年龄的人放在同一个list中: Map<Integer, List<Person>> personGroups = Stream.generate(new PersonSupplier()). limit(100). collect(Collectors.groupingBy(Person::getAge)); Iterator it = personGroups.entrySet().iterator(); while (it.hasNext()) { Map.Entry<Integer, List<Person>> persons = (Map.Entry) it.next(); System.out.println("Age " + persons.getKey() + " = " + persons.getValue().size()); } //打印结果: Age 0 = 2 //年龄为0的有两个人 Age 1 = 2 Age 5 = 2 Age 8 = 1 Age 9 = 1 Age 11 = 2 > 案例: 按照未成年人和成年人归组 Collectors.partitioningBy方法 Map<Boolean, List<Person>> children = Stream.generate(new PersonSupplier()). limit(100). collect(Collectors.partitioningBy(p -> p.getAge() < 18)); System.out.println("Children number: " + children.get(true).size()); System.out.println("Adult number: " + children.get(false).size()); //打印结果: Children number: 23 Adult number: 77 > 在使用条件“年龄小于 18”进行分组后可以看到,不到 18 岁的未成年人是一组,成年人是另外一组。partitioningBy 其实是一种特殊的 groupingBy,它依照条件测试的是否两种结果来构造返回的数据结构,get(true) 和 get(false) 能即为全部的元素对象。 #### Stream流的特性总结: #### * 不是数据结构 * 他没有内部存储,他只是用操作管道从soruce(数据结构,数组,generator function,IO channel)抓取数据。 * 所有 Stream 的操作必须以 lambda 表达式为参数 * 不支持索引访问 * 惰性化 * 很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。 * Intermediate 操作永远是惰性化的。 * 并行能力 * 一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。 [stream]:
相关 Java8新特性 stream流 JDK1.8的新特性 1. 前言 JDK1.8已经发布很久了,在很多企业中都已经在使用。并且Spring5、SpringBoot2.0都推荐使用JDK1.8以上版本 朱雀/ 2024年03月31日 13:17/ 0 赞/ 44 阅读
相关 Java8新特性 Stream流 [Java 8 Stream 菜鸟教程][Java 8 Stream] > Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Str 缺乏、安全感/ 2024年03月31日 09:10/ 0 赞/ 23 阅读
相关 Java8新特性Stream流详解 > Stream流是Java8新特性中实践起来最让人舒服的功能,它让我们告别了繁琐的for循环迭代,所以Stream是我们必须要掌握的一个技术特性 > 此文将带着你 迷南。/ 2023年09月24日 13:53/ 0 赞/ 94 阅读
相关 Java8新特性Stream流详解 > 陈老老老板 说明:新的专栏,本专栏专门讲Java8新特性,把平时遇到的问题与Java8的写法进行总结,需要注意的地方都标红了,一起加油。 > 本文是介绍Java8新特性 桃扇骨/ 2023年09月24日 13:37/ 0 赞/ 102 阅读
相关 jdk8 新特性之 Stream流 1、第一个使用流示例 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9 骑猪看日落/ 2023年03月13日 13:17/ 0 赞/ 27 阅读
相关 java8新特性stream流 本文章 转载自头条网, 只是觉得好用很详细,所以自己收集 做下笔记,不做任何商业用途,不收任何费用,不喜勿喷。 致敬 头条@程序猿的内心独白 1. Stream初体验 太过爱你忘了你带给我的痛/ 2023年01月15日 09:26/ 0 赞/ 147 阅读
相关 Java8新特性之Stream流 什么是Stream Stream是Java8中处理集合的关键抽象概念,他可以指定你希望对集合进行非常复杂的查找,过滤和映射数据等操作。Steam是一种高效的处理数 刺骨的言语ヽ痛彻心扉/ 2022年05月16日 11:36/ 0 赞/ 223 阅读
相关 Java8新特性之Stream流实战 Stream 流的各种实战 学习了 Stream 之后,我们会遇到一个问题,就是如何使用Stream 流来进行我们的数据处理。 准备工作 创建一个 Employ 类 川长思鸟来/ 2022年03月19日 04:36/ 0 赞/ 274 阅读
相关 JAVA8新特性之Stream流 > 一,问题 上文中,大概了解了lambda表达式的书写,然后再这里再看一下stream流的使用。 > 二,解决方案 package language.strea ╰半橙微兮°/ 2022年01月14日 09:47/ 0 赞/ 316 阅读
还没有评论,来说两句吧...