ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、视频、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# Stream流 [TOC] ## 导学 Stream流的使用主要针对集合和数组等数据容器,Stream流的作用包括对数据容器进行过滤,映射,去重,排序等转换操作,以及将Stream流转为集合或统计等聚合操作。 所以Stream流的出现,就是为了弥补jdk中集合类库的缺陷。以下有些Stream流的使用注意点 1. Stream流不是一种数据结构,不保存数据,它只是在原数据集上定义了一组操作。 2. 这些操作是惰性的,即每当访问到流中的一个元素,才会在此元素上执行这一系列操作。 3. Stream不保存数据,故每个Stream流只能使用一次。 ~~~ //stream针对于集合进行操作 List<String> list = new ArrayList<>(); list.add("red"); list.add("orange"); list.add("red"); list.add("blue"); list.add("white"); list.add("black"); list.add("gray"); list.add("red"); list.add("green"); Stream<String> sm1 = list.stream();//Stream<String>和List<String>只是长得像,但Stream不会作为一个数据容器。 Stream<String> sm2 = sm1.distinct();//由sm1转化为sm2流对象的时候,并不会对集合中的元素产生任何的操作 Stream<String> sm3 = sm2.map(str -> str += "123"); List<String> lst = sm3.collect(Collectors.toList());//只有在对sm3流对象进行聚合时。才会真正执行元素聚合操作 //List<String> lst = list.stream().distinct().map(str -> str += "123").collect(Collectors.toList()); System.out.println(list); System.out.println(lst); ~~~ **关于Stream流的使用策略** 1. 首先需要构建Stream流 2. 每次操作Stream流,不会改变原有的Stream流对象,只会生成新的Stream流对象,而且可以把执行聚合任务之前的Stream流对象认为是用来记录转换操作的。 3. Stream流中很多方法都会返回新Stream流,所以Stream流编程大多是链式编程风格 4. 最后,在操作完Stream流后,基本会对Stream流进行聚合操作(执行真正的计算,并返回新集合等内容) 5. 在执行完聚合操作之后,Stream流就会被关闭,所以对同一个Stream流而言,转换操作可以执行多次, 但聚合操作只能执行一次。 ## Stream流的使用 ### Stream流对象的创建 Stream流对象的创建主要有两大类,第一种是基于Stream流本身的静态方法,第二种基于数组和集合的`stream()`方法。 1. Stream中的静态方法 1.1 `generate()`方法和`iterate()`方法 ![](https://img.kancloud.cn/2e/47/2e472a18720a2da8815a72bfe5b119bb_1442x109.png) 这两个方法都需要自定义元素的生成方式,所以使用的还是比较少的。 ~~~ public class Test { public static void main(String[] args) { Stream<Integer> sm = Stream.generate(new Add()); // 注意:无限序列必须先变成有限序列再打印: sm.limit(20).forEach(System.out::println); } } class Add implements Supplier<Integer> { int n = 0; public Integer get() { n++; return n; } } ~~~ 1.2 `of()`方法 ![](https://img.kancloud.cn/53/db/53dbbaa84c7c5aa4e86b32fc063e77cb_1446x109.png) 在Stream中,存在两个重载的静态`of()`方法,这两个方法,一个接收任意类型的可变参数列表作为参数,另一个接收单个任意类型参数。 ~~~ public class StreamTest { public static void main(String[] args) { Stream<String> s = Stream.of("1","2","3","4"); Stream<String> s1 = Stream.of("2"); } } ~~~ 2. 数组和集合的`stream()`方法 在日常的使用中,Stream主要用于集合和数组操作,所以这里的两个stream方法也是用的比较多的。 ~~~ public class StreamTest { public static void main(String[] args) { String[] arr = new String[]{"a","b","c"}; Stream<String> s = Arrays.stream(arr); List<String> list = new ArrayList<>(); Stream<String> s1 = list.stream(); } } ~~~ ### Stream流中的转换操作 Stream流中的转换操作,其实是用来记录Stream对元素的转换操作,本质上并不涉及对元素的直接实质化操作,仅仅是起一个记录的作用,与之相对的是聚合操作。转换操作通常会由一个Stream流对象,返回一个新的Stream流对象,以下是一些常用的Stream对象的常用方法。 * **`forEach()`方法** `forEach()`方法比较特殊,该方法虽然是一个转换操作,但该方法没有返回值。 ~~~ public class StreamTest { public static void main(String[] args) { List<String> list = new ArrayList<>(); //asList方法生成的集合不可改动,所以将该集合的元素添加到另一个集合中 list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"})); list.stream().forEach(System.out::println); } } ~~~ * **`map()`方法** `map()`:将对Stream序列中的每一个元素进行操作,参数为Function接口对象,要求传入的是对元素的执行内容,在map中,Function接口对象的apply方法的参数的类型是Stream流中,元素的类型,因为该方法接收的是Stream流中的元素。 ~~~ public class StreamTest { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"})); list.stream().map(str -> str += "颜色").forEach(System.out::println); } } ~~~ 问:执行完map方法后,新生成的stream对象中,元素的个数有没有变化?元素的类型有没有变化? 答:**元素个数不变,因为map只操作元素,不删除元素。元素类型有可能变化。** * **`flatMap()`方法** `flatMap()`方法针对原有Stream流对象中的数据容器进行扁平化操作,将每个数据容器转换为小的Stream对象,最终将所有小的Stream流对象合并为新的大的Stream流对象。 ~~~ public class StreamTest { public static void main(String[] args) { List<String[]> list = new ArrayList<>(); list.addAll(Arrays.asList(new String[][]{{"red","blue"},{"green","yellow","red","black"}})); list.stream().flatMap(arr -> Arrays.stream(arr)).forEach(System.out::println); } } ~~~ * **`filter()`方法** `filter()`方法将对Stream序列中的每一个元素进行过滤,参数为Predicate接口对象,要求传入的是对元素的过滤规则,在filter中,Predicate接口对象中存在test方法,该方法接收stream中的元素作为参数,返回布尔类型的值。如果test方法返回为true,则stream流中的元素得以保存,如果返回为false,则清除该元素。 ~~~ public class StreamTest { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"})); list.stream().filter(str -> !"red".equals(str)).forEach(System.out::println); } } ~~~ 问:执行完filter方法后,新生成的stream对象中,元素的个数有没有变化?元素的类型有没有变化? 答:**元素个数可能变化,因为filter会执行清除元素操作。元素类型没有变化。** * **`sorted()`方法** `sorted()`方法对stream流中的元素进行排序,建议使用`Collections.sort()` * **`distinct()`方法** `distinct()`方法用于去除重复的元素,对于重复元素的判断,调用的是equals方法。该方法无需参数 * **`skip()`方法 & `limit()`方法** `skip()`方法用于跳过当前Stream的前N个元素。 `limit()`方法用于截取当前Stream最多前N个元素,两个方法都是接收int类型参数。 ~~~ public class StreamTest { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"})); list.stream().skip(3).map(str -> str += 1).forEach(System.out::println); System.out.println("===================="); list.stream().limit(3).map(str -> str += 1).forEach(System.out::println); } } ~~~ 思考题:如何只针对Stream流中,除首尾之外的其他元素进行处理? * **`concat()`方法** `concat()`方法将两个泛型相同的stream流对象,合并为一个stream流对象 * **`allMatch()`方法 & `anyMatch()`方法** 这两个方法用于测试元素是否都满足条件/是否有一个满足条件,这两个方法的返回值都是Boolean类型。 ~~~ public class StreamTest { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.addAll(Arrays.asList(new String[]{"red","blue","green","yellow","red","black"})); System.out.println(list.stream().anyMatch(str -> "red".equals(str))); System.out.println(list.stream().allMatch(str -> "red".equals(str))); } } ~~~ ### 聚合操作 Stream流中的聚合操作,实际上用来真正执行元素的计算任务的,或是可以用来对聚合完的元素进行整合输出。在Stream流中,有很多的聚合操作,在这里我们主要挑出常用的两大类聚合操作来说明: * **`reduce()`方法** `reduce()`方法可以用来对Stream中的元素进行聚合,我们可以用`reduce()`方法来进行元素的求和求积等操作,还可以进行一些转换元素类型的高级操作。`reduce()`方法本身具有三种重载方法: ![](https://img.kancloud.cn/56/59/5659f8e26f48d911ce2e822a171d05df_1439x159.png) **以下,通过一个问题来学习`reduce()`方法,问,如何将Person集合中,各元素的人名取出来组成一个姓名集合?** * 单参`reduce()` 单参`reduce()`方法,返回值为Optional类型,这是因为在不给定默认值的情况下,Stream本身并不会去确定`reduce()`方法的运行结果,需要调用者自己去确定。 ~~~ public class StreamTest { public static void main(String[] args) { List<Person> list = new ArrayList<>(); list.add(new Person("张三",23)); list.add(new Person("李四",23)); list.add(new Person("王五",23)); //Optional类是一个容器类,它可以保存模板类型T的值,也可以保存null。 Optional<List<String>> lt = list.stream().map(person -> { List<String> lst = new ArrayList<>(); lst.add(person.name); return lst; }).reduce((lst1, lst2) -> { lst1.addAll(lst2); return lst1; }); if(lt.isPresent()) { List<String> ls = lt.get(); System.out.println(ls); } } } class Person { public String name; public int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } } ~~~ 在上述代码中,我们在`Stream`流中,先将三个`Person`类型的数据,转变为三个`List<String>`类型的数据,然后在将数据使用`reduce()`整合为一个`List`。但是此时并没有指定默认值,所以此时可能出现结果为null的情况,所以`reduce()`方法将返回值设置为可以存储null的`Optional`容器。 参考资料:**Java8新特性 Optional的正确使用姿势** https://www.jianshu.com/p/59db4b2c7543 * 双参`reduce()` 对于双参的方法,我们在使用前,给定了默认值,这个默认值需要和Stream中的元素是同一类型的 ~~~ public class StreamTest { public static void main(String[] args) { List<Person> list = new ArrayList<>(); list.add(new Person("张三",23)); list.add(new Person("李四",23)); list.add(new Person("王五",23)); List<String> lt = list.stream().map(person -> { List<String> lst = new ArrayList<>(); lst.add(person.name); return lst; }).reduce(new ArrayList<String>(), (lst1, lst2) -> { lst1.addAll(lst2); return lst1; }); System.out.println(lt); } } class Person { public String name; public int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } } ~~~ * 三参`reduce()` 实际上,在前两个方法的使用中我们发现,`reduce()`方法有着很多的使用限制,包括返回值类型需要和元素类型相同等问题,实际上,限制了我们对Stream中元素的进一步修改。在三参方法中,第三个参数是一个BinaryOperator类型的表达式。在常规情况下我们可以忽略这个参数,随便指定一个表达式即可,目的是为了通过编译器的检查,因为在常规的Stream中它并不会被执行到,然而, 虽然此表达式形同虚设,可是我们也不是把它设置为null,否则还是会报错。 在并行Stream中,此表达式则会被执行到。 ~~~ public class StreamTest { public static void main(String[] args) { List<Person> list = new ArrayList<>(); list.add(new Person("张三",23)); list.add(new Person("李四",23)); list.add(new Person("王五",23)); List<String> lt = list.stream().map(person -> person.name).reduce(new ArrayList<String>(), (lst, per) -> { lst.add(per); return lst; }, (lst, per) -> null); System.out.println(lt); } } class Person { public String name; public int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } } ~~~ 那么,我们可以看到使用三参的方法,使我们代码少写很多,同样也显得优雅很多。那是否可以以一种更优雅的方式呢? * **`collect()`方法** collect()`方法有两个重载方法,但用的最多的还是和`java.util.stream.Collectors`类共同使用的方法。比如我们去改写上面问题的代码 ~~~ List<String> lt = list.stream().map(person -> person.name).collect(Collectors.toList()); ~~~ 这样的代码同样可以去实现`reduce()`方法中一样的效果,其中主要依靠的就是`Collectors`类中自带方法的作用。 * **`toCollection()`方法** 该方法用于将流中的元素全部放置到一个集合中返回,包括List,Set,Queue ~~~ public class StreamTest { public static void main(String[] args) { List<Person> list = new ArrayList<>(); list.add(new Person("张三",23)); list.add(new Person("李四",23)); list.add(new Person("王五",23)); list.add(new Person("王五",23)); //List<String> lt = list.stream().map(person -> person.name).collect(Collectors.toCollection(ArrayList::new)); Set<String> lt = list.stream().map(person -> person.name).collect(Collectors.toCollection(HashSet::new)); System.out.println(lt); } } class Person { public String name; public int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } } ~~~ * **`toList()`方法 & `toSet()`方法** 这两个方法用于明确将Stream流聚合为List集合或Set集合 ~~~ List<String> lt = list.stream().map(person -> person.name).collect(Collectors.toList()); Set<String> lt = list.stream().map(person -> person.name).collect(Collectors.toSet()); ~~~ * **`joining()`方法** 用于拼接流中的元素,如果元素不是String类型,将会以String类型的形式被拼接,该方法可以指定分隔符。 ~~~ String lt = list.stream().map(person -> person.name).collect(Collectors.joining(",")); ~~~ * **`mapping()`方法** 用于对流中所有的元素执行转换,该方法接收两个参数,第一个参数为Function接口,将会执行其中的apply抽象方法,作为元素转换的执行内容。第二个参数为Collector接口的实现类,该实现类可以指定元素的聚合。 `mapping()`方法其实和`map()`方法有些类似,都是用于对元素进行映射操作。但不同的地方在于`mapping()`方法有两个参数。第一个参数用于表示对元素的转换等操作, 第二个参数一般用于表示对元素的聚合操作,所以该方法也属于聚合作用。 ~~~ public class StreamTest { public static void main(String[] args) { List<Person> list = new ArrayList<>(); list.add(new Person("张三",23)); list.add(new Person("李四",23)); list.add(new Person("王五",23)); list.add(new Person("王五",23)); //List<String> lt = list.stream().map(per -> per.name).collect(Collectors.toList()); List<String> lt = list.stream().collect(Collectors.mapping(per -> per.name, Collectors.toList())); System.out.println(lt); } } ~~~ 其实从上述代码可以看出,在`mapping()`方法中,我是把原有的map方法和collect方法结合起来了,那是否就意味着mapping方法有点多此一举呢?其实不是这样,mapping方法多用于在Stream转换完成后,需要最终进行转换并聚合的情况。 * **`collectingAndThen()`方法** 用于对聚合完的数据,进行再次聚合,其实第二次聚合可以认为是已聚合好数据的一种转换。 ~~~ int lt = list.stream().map(per -> per.name).collect(Collectors.collectingAndThen(Collectors.toList(), lst -> lst.size())); ~~~ * **`counting()`方法** 用于对聚合完的数据进行统计总个数 ~~~ long lt = list.stream().map(per -> per.name).collect(Collectors.counting()); ~~~ * **`summingInt()`方法 & `summingLong()`方法 & `summingDouble()`方法** 这几个方法,用于对流中的元素进行求和,该方法的参数作用为将流传过来的参数改为对应的类型 ~~~ //int lt = list.stream().map(per -> per.age).collect(Collectors.summingInt(Integer::valueOf)); long lt = list.stream().map(per -> per.age).collect(Collectors.summingLong(Long::valueOf)); ~~~ `averagingInt`/`averagingLong`/`averagingDouble`:求平均数,使用方法同上 * **`toMap()`方法** 该方法用于将Stream中的元素转为Map集合,一般而言,我们会尽量使用三参的方法,如果确定key值不会重复,可以使用两参的方法。 在三参方法中,第一个参数用于生成key值,第二个参数用于生成value值,第三个参数用于提供key值相同时的解决方案。 ![](https://img.kancloud.cn/46/17/46173b3c3ba394893c2eba6b48f20ee1_1446x182.png) ~~~ Map<String, Integer> lt = list.stream().collect(Collectors.toMap(per -> per.name, per -> per.age, (k1, k2) -> k2)); ~~~ * **`groupingBy()`方法** 该方法用于对Stream流的元素进行分组,该方法存在重载方法,主要使用的是单参方法,只需要传入分组依据即可。 ~~~ Map<Object, List<Person>> lt = list.stream().collect(Collectors.groupingBy(per -> per.age)); ~~~