美文网首页
Java8 新特性(二)- Stream

Java8 新特性(二)- Stream

作者: _水杉 | 来源:发表于2020-01-16 11:05 被阅读0次

    Stream 用来处理集合数据的,通过 stream 操作可以实现 SQL 的拥有的大部分查询功能

    Java8 API 官方文档

    下面借助例子,演示 stream 操作

    Java userList 列表

    private List<User> userList = Arrays.asList(
        new User(101, "小明", 10, "男", "青海省", "西宁市"),
        new User(102, "小青", 12, "女", "宁夏回族自治区", "银川市"),
        new User(103, "小海", 8, "男", "西藏自治区", "拉萨市"),
        new User(108, "阿刁", 18, "女", "西藏自治区", "拉萨市"),
        new User(104, "小阳", 9, "女", "新疆维吾尔自治区", "乌鲁木齐市"),
        new User(105, "小强", 14, "男", "陕西省", "西安市"),
        new User(106, "小帅", 15, "男", "河北省", "石家庄市"),
        new User(107, "小云", 15, "女", "河北省", "石家庄市")
    );
    

    MySQL user 表数据

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user`  (
      `id` int(11) PRIMARY KEY,
      `name` varchar(20),
      `age` int(2),
      `gender` varchar(10),
      `province` varchar(100),
      `city` varchar(100)
    ) ;
    
    INSERT INTO `user` VALUES (101, '小明', 10, '男', '青海省', '西宁市');
    INSERT INTO `user` VALUES (102, '小青', 12, '女', '宁夏回族自治区', '银川市');
    INSERT INTO `user` VALUES (103, '小海', 8, '男', '西藏自治区', '拉萨市');
    INSERT INTO `user` VALUES (104, '小阳', 9, '女', '新疆维吾尔自治区', '乌鲁木齐市');
    INSERT INTO `user` VALUES (105, '小强', 14, '男', '陕西省', '西安市');
    INSERT INTO `user` VALUES (106, '小帅', 15, '男', '河北省', '石家庄市');
    INSERT INTO `user` VALUES (107, '小云', 15, '女', '河北省', '石家庄市');
    

    查询字段 select - map

    // select id from user
    userList.stream()
        .map(e -> e.getId())
        .forEach(System.out::println);
    

    至于如何实现 select id, name from user 查询多字段在下面 collector 收集器会详细讲解

    条件 where - filter

    // select * from user where age<10
    userList.stream()
            .filter(e-> e.getAge() < 10)
            .forEach(System.out::println);
    
    // select * from user where age<10 and gender='男'
    userList.stream()
            .filter(e->e.getAge() < 10)
            .filter(e->e.getGender()=="男")
            .forEach(System.out::println);
    

    最值、总和、数量、均值(max, min, sum, count, average)

    // select max(age), min(age), sum(age), count(age), avg(age) from user
    // max
    Optional<Integer> maxAge = userList.stream()
                                    .map(e -> e.getAge())
                                    .max(Comparator.comparingInt(x -> x));
    // 等同于
    // Optional<Integer> maxAge =  userList.stream()
    //  .map(e -> e.getAge())
    //  .max((x, y) -> x-y);
    
    // min
    Optional<Integer> minAge = userList.stream()
                                    .map(e -> e.getAge())
                                    .min(Comparator.comparingInt(x -> x));
    // sum
    Optional<Integer> sumAge = userList.stream()
                                    .map(e -> e.getAge())
                                    .reduce((e, u) -> e + u);
    // count
    long count = userList.stream()
                    .map(e -> e.getAge())
                    .count();
    // 平均值=总和/数量
    

    排序 order by - sorted

    // select * from user order by age
    userList.stream()
            .sorted(Comparator.comparingInt(User::getAge))
            .forEach(System.out::println);
    

    分页 limit - skip、limit

    // select * from user limit 5
    userList.stream()
            .limit(5)
            .forEach(System.out::println);
    
    // select * from user limit 5, 5
    userList.stream()
            .skip(5)
            .limit(5)
            .forEach(System.out::println);
    
    // select * from user order by age limit 1
    userList.stream()
            .sorted(Comparator.comparingInt(User::getAge))
            .limit(1)
            .forEach(System.out::println);
    // 或者
    Optional<User> minAgeUser = userList.stream()
                                    .sorted(Comparator.comparingInt(User::getAge))
                                    .findFirst();
    

    是否存在 exists - anymatch

    // select exists(select * from user where name='小海')
    // 有没有名字叫“小海”的用户
    boolean exists0 = userList.stream()
                            .anyMatch(e -> e.getName().equals("小海"));
    
    // select not exists(select * from user where name='小海')
    // 是不是没有名字叫“小海”的用户
    boolean exists1 = userList.stream()
                            .noneMatch(e -> e.getName().equals("小海"));
    
    // 是不是所有用户年龄都小于10岁
    boolean exists2 = userList.stream()
                            .allMatch(e -> e.getAge() < 10);
    

    收集操作 collect

    收集操作就是遍历 stream 中的元素,并进行累加处理,即归约 reduction

    归约的定义

    A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list.

    前面提到的 max() min() count() reduce() 都属于 reduction operation

    collect() 又和前面这几种归约操作有所区别,它是 Mutable reduction 动态归约

    动态归约的定义

    A mutable reduction operation accumulates input elements into a mutable result container, such as a Collection or StringBuilder, as it processes the elements in the stream

    区别:动态归约将结果放进 Collection StringBuilder 这样的动态容器中,所以称为动态归约。

    Stream 接口提供了两个 collect() 方法

    <R> R collect(Supplier<R> supplier,
                    BiConsumer<R, ? super T> accumulator,
                    BiConsumer<R, R> combiner);
    
    <R, A> R collect(Collector<? super T, A, R> collector);
    

    我们只需理解了第一个方法,第二个方法就手到擒来了

    理解第一个 collect 方法,强烈建议阅读文档 动态归约的定义,下面只简单的介绍一下它

    三个参数:

    • 供给者 supplier:负责提供动态容器,例如 Collectors、StringBuilder
    • 累加器 accumulator:负责将流中的元素做累加处理
    • 合并者 combiner :负责将两个容器的元素合并在一起

    在串行流中,combiner 根本没有执行,所以随便写点啥满足参数对象就行。
    如果说串行流是单线程,那么并行流就是多线程了

    举个例子:

    
         ArrayList<String> strings = new ArrayList<>();
         for (T element : stream) {
             strings.add(element.toString());
         }
        // 等同于
        ArrayList<String> strings = stream.collect(() -> new ArrayList<>(),
                                                    (c, e) -> c.add(e.toString()),
                                                    (c1, c2) -> c1.addAll(c2));
    

    与其传递三个参数这么麻烦,还不如直接传递一个对象呢!
    这就是第二个 collect() 方法的由来,使用收集器 Collector 来替代三个参数

    实际上,我们一般不需要自己创建 Collector 对象,Java8 提供了一个 Collectors 类,专门提供收集器 Collector 对象。毕竟我们平时能够使用到的收集操作也就那几种:转为集合对象、分组、统计。

    下面以例子演示


    在初看 stream 操作的时候,我被什么创建、中间操作、终止操作、不会改变原对象给弄晕了,我根本不关心这些,我的第一想法是怎么将操作后的数据导出来,重新变成集合对象。

    toCollection

    不使用收集器的情况下:

    List<User> subUserList1 = userList.stream()
                    .filter(e -> e.getAge() < 10)
                    .filter(e -> e.getGender() == "男")
                    .collect(() -> new ArrayList<>(),
                            (c, e) -> c.add(e),
                            (c1, c2) -> c1.addAll(c2));
    

    在 collect() 方法第二个参数累加器 accumulator (c, e) -> c.add(e) 这里,对流中元素进行了遍历,所以可以把流中元素添加到任意的集合容器中,List、Set、Map 等等

    使用 Collectors 工具类提供的收集器:

    // toList()
    List<User> list = userList.stream()
                    .filter(e -> e.getAge() < 10)
                    .filter(e -> e.getGender() == "男")
                    .collect(Collectors.toList());
    
    // toSet()
    Set<User> set = userList.stream()
                    .filter(e -> e.getAge() < 10)
                    .filter(e -> e.getGender() == "男")
                    .collect(Collectors.toSet());
    
    // toCollection(),想要返回什么容器,就 new 一个
    ArrayList<User> collection = userList.stream()
                    .filter(e -> e.getAge() < 10)
                    .filter(e -> e.getGender() == "男")
                    .collect(Collectors.toCollection(
                        () -> new ArrayList<>()
                    ));
    

    这里插播一条新闻:如何将流转为数组?

    Stream 提供了方法 toArray()

    Object[] toArray();
    <A> A[] toArray(IntFunction<A[]> generator);
    

    小试牛刀:

    Object[] nameArray = userList.stream()
            .map(e -> e.getName())
            .toArray();
    Arrays.stream(nameArray)
            .forEach(System.out::println);
    // 转为 User 对象数组
    User[] users = userList.stream()
            .filter(e -> e.getGender() == "女")
            .toArray(User[]::new);
    Arrays.stream(users)
            .forEach(System.out::println);
    

    toStringBuilder

    不使用收集器的情况下:

    StringBuilder joinName = userList.stream()
                    .map(e -> e.getName())
                    .collect(StringBuilder::new,
                            (s, e) ->  s = s.length() > 0 ? s.append("-" + e) : s.append(e),
                            (s1, s2) -> s1.append(s2)
                    );
    

    谁能告诉我在Java中怎么单独使用三元运算符?s = s.length() > 0 ? s.append("-" + e) : s.append(e) 我想把 s = 省略掉,但 Java 中不行

    使用 Collectors 类提供的收集器:

    String joinName1 = userList.stream()
                    .map(e -> e.getName())
                    .collect(Collectors.joining());
    
    String joinName2 = userList.stream()
        .map(e -> e.getName())
        .collect(Collectors.joining("-"));
    
    String joinName3 = userList.stream()
        .map(e -> e.getName())
        .collect(Collectors.joining("-", "[", "]"));
    

    至于 Collectors.joining() 参数分别代表什么含义,看一下它们的参数名称,就明白了

    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,    // 分隔符
                                                                 CharSequence prefix,   // 前缀
                                                                 CharSequence suffix)   // 后缀
    

    toMap

    在 Collectors 中一共有3个 toMap(),它们用来处理不同的问题

    两个参数的 toMap

     Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                        Function<? super T, ? extends U> valueMapper) {
            return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
        }
    

    参数 keyMapper 用来获取key;valueMapper 用来获取 value

    它的内部调用了四个参数的 toMap() 方法

    例子

    Map<Integer, User> map1 = userList.stream()
        .collect(Collectors.toMap(e -> e.getId(), Function.identity()));
    System.out.println(map1);
    // Function.identity() 等价于 e -> e
    
    // select id, name, gender from user
    Map<Integer, Map<String, Object>> map2 = userList.stream()
        .collect(Collectors.toMap(e -> e.getId(), e -> {
            Map<String, Object> map = new HashMap<>();
            map.put("gender", e.getGender());
            map.put("name", e.getName());
            map.put("id", e.getId());
            return map;
        }));
    System.out.println(map2);
    

    你:如果 key 冲突了咋办?
    Java8:你想咋办就咋办

    三个参数的 toMap

    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                        Function<? super T, ? extends U> valueMapper,
                                        BinaryOperator<U> mergeFunction) {
            return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
        }
    

    第三个参数 mergeFunction 就是用来处理 key 键冲突的

    内部也是调用了四个参数的 toMap() 方法

    例子

    // 如果 key 冲突,那么将冲突的 value 值拼接在一起
    Map<String, String> map3 = userList.parallelStream()
                    .collect(Collectors.toMap(
                        e -> e.getGender(), 
                        e -> e.getName(), 
                        (o1, o2) -> o1 + ", " + o2
                        )
                    );
    System.out.println(map3);
    

    你:我想自己 new 一个 Map 对象

    四个参数的 toMap

    Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction,
                                Supplier<M> mapSupplier) 
    

    参数 mapSupplier 用来提供返回容器

    例子

    LinkedHashMap<String, String> map4 = userList.parallelStream()
                    .collect(Collectors.toMap(e -> e.getGender(), e -> e.getName(), (o1, o2) -> o1 + ", " + o2, LinkedHashMap::new));
    System.out.println(map4);
    

    reducing

    单参数和两参数的 reducing()

    Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op)
    Collector<T, ?, U> reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
    

    以例子具体解释这两个方法

    Optional<String> names1 = userList.stream()
                    .map(User::getName)
                    .collect(Collectors.reducing((e1, e2) -> e1 + "," + e2));
    System.out.println(names1.get());
    
    // 等同于
    String names2 = userList.stream()
                .collect(Collectors.reducing(
                        "", (e) -> e.getName(), (e1, e2) -> e1 + "," + e2)
                        );
    System.out.println(names2);
    

    输出结果:

    小明,小青,小海,阿刁,小阳,小强,小帅,小云
    ,小明,小青,小海,阿刁,小阳,小强,小帅,小云
    

    参数 identity 表示返回结果的初始值

    三参数的 reducing()

    reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)
    

    identity 是初始值,mapper 会对元素先进行一次处理,然后 op 对元素进行归约操作

    注意: 返回类型要和参数 identity 的一致。
    你也许会纳闷,为什么有的返回一个 Optional<String> 类型数据,而有的就返回了 String
    因为含有参数 identity 的 reduing 方法中返回值有初始值,也就是 identity,所以不会出现空的情况

    下面Collectors 提供的一些常用归约收集器

    // minBy、maxBy
    Optional<User> minAgeUser = userList.stream()
                    .collect(Collectors.minBy((o1, o2) -> o1.getAge() - o2.getAge()));
    
    // counting
    Long count = userList.stream()
        .collect(Collectors.counting());
    
    // summingInt、summingLong、summingDouble、
    Integer sumAge = userList.stream()
        .collect(Collectors.summingInt(User::getAge));
    
    // averagingInt、averagingLong、averagingDouble
    // 平均值内部是总值/数量,所以返回值是浮点数 dobule
    Double avgAge = userList.stream()
        .collect(Collectors.averagingInt(User::getAge));
    
    

    你也许觉得每次都要执行一遍 minBy、maxBy、counting、summingXxx、averagingXxx 这些太麻烦了,有没有一次执行就获取所有这些方法结果?
    有的。这就是 summarizingXxx

    Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)
    Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)
    Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)
    

    这里不演示了,实际上你看一下 XxxSummaryStatistics 这些类就明白了,比如

    public class IntSummaryStatistics implements IntConsumer {
        private long count;
        private long sum;
        private int min = Integer.MAX_VALUE;
        private int max = Integer.MIN_VALUE;
        ...
    }
    

    group by

    最最激动人心的时候到了,我们要使用分组了!!!

    Map<String, List<User>> map = userList.stream()
                    .collect(Collectors.groupingBy(User::getGender));
    

    SQL 中的 group by 结果集中只能包含分组字段和聚合函数计算结果,这段代码比它更加全面

    我们使用如下语句输出结果

    map.keySet().stream()
            .forEach((e) -> {
                System.out.println(e + "=" + map.get(e));
            });
    

    显示结果:

    女=[User{id=102, name='小青', age=12, gender='女', province='宁夏回族自治区', city='银川市'}, User{id=108, name='阿刁', age=18, gender='女', province='西藏自治区', city='拉萨市'}, User{id=104, name='小阳', age=9, gender='女', province='新疆维吾尔自治区', city='乌鲁木齐市'}, User{id=107, name='小云', age=15, gender='女', province='河北省', city='石家庄市'}]
    男=[User{id=101, name='小明', age=10, gender='男', province='青海省', city='西宁市'}, User{id=103, name='小海', age=8, gender='男', province='西藏自治区', city='拉萨市'}, User{id=105, name='小强', age=14, gender='男', province='陕西省', city='西安市'}, User{id=106, name='小帅', age=15, gender='男', province='河北省', city='石家庄市'}]
    
    

    它真的分组了!!这是真正的分组

    那怎么对分组中的元素进行操作呢,像 SQL 那样??

    完全不用担心,Collectors 提供了三个 groupBy 方法返回分组收集器

    Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T,? extends K> classifier)
        
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T,? extends K> classifier, 
                                          Collector<? super T,A,D> downstream)
        
    Collector<T, ?, M> groupingBy(Function<? super T,? extends K> classifier, 
                                  Supplier<M> mapFactory, 
                                  Collector<? super T,A,D> downstream)
    

    让我们放飞想象的翅膀,思考一下这几个参数分别有什么用。

    downstream ?有 down 就表示有 up。那么谁是 upstream,很明显是 userList.stream,那么 downstream 就是分组集合的流喽。猜测 downstream 收集器是对分组中的元素进行归约操作的,就像是分组 SQL 语句字段中的聚合操作一样。

    // select gender, count(*) from user group by gender
    Map<String, Long> map2 = userList.stream()
                    .collect(Collectors.groupingBy(User::getGender, Collectors.counting()));
    System.out.println(map2);
    

    输出结果确实不出所料!这就是证明参数 downstream 确实是分组集合元素的收集器。

    Supplier<M> mapFactory 这函数式接口方法不会有参数传入,所以不会操作集合元素;它只是返回一个变量。同志们,注意观察三个方法返回值,前二者都指定了 Map 作为归约操作的返回类型,而第三个要我们自己定义,使用 mapFactory 提供返回的数据容器

    两参数的 groupingBy 方法其实是调用了三参数的 groupingBy 方法(而单参数 groupingBy 调用了两参数的 groupingBy)

        Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                              Collector<? super T, A, D> downstream) {
            return groupingBy(classifier, HashMap::new, downstream);
        }
    

    groupingBy 经常使用 Collectors.mapping() 处理分组集合

    Map<String, List<Map<String, Object>>> map4 = userList.stream()
        .collect(Collectors.groupingBy(
            User::getGender,
            Collectors.mapping(e -> {
                Map<String, Object> m = new HashMap<>();
                m.put("name", e.getName());
                m.put("id", e.getId());
                return m;
            }, Collectors.toList())
        ));
    System.out.println(map4);
    

    输出结果:

    {女=[{name=小青, id=102}, {name=阿刁, id=108}, {name=小阳, id=104}, {name=小云, id=107}], 男=[{name=小明, id=101}, {name=小海, id=103}, {name=小强, id=105}, {name=小帅, id=106}]}
    

    partitionBy

    实际上也是分组,只不过 partitionBy 是按照布尔值(真假)来分组

    Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
        return partitioningBy(predicate, toList());
    }
    
    Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                        Collector<? super T, A, D> downstream)
    

    例子:大于10岁为一组,小于等于10的为一组

    Map<Boolean, List<User>> map1 = userList.stream()
                    .collect(Collectors.partitioningBy(e -> e.getAge() > 10));
    

    例子:统计大于10岁的有多少人,小于等于10岁的有多少人

    Map<Boolean, Long> map2 = userList.stream()
                .collect(Collectors.partitioningBy(e -> e.getAge() > 10, Collectors.counting()));
    

    第二个参数 downstream 用来处理分组集合

    结语

    Java8 提供的 stream 几乎是穷尽了所有集合元素能有的操作,起码是穷尽了我脑海里对集合元素操作的所有想象

    这篇文章也列举了 stream 绝大部分的功能,尽量写得通俗易懂,但读者理解起来可能还是有模糊的地方,这时建议大家参考 Java8 API 官方文档 ,多做几个 Demo 加深理解

    不要过度使用

    stream 是为了方便集合操作,简化代码而推出的,提升代码执行效率并不是它的目的。

    虽然,并行流会对代码的执行效率有较大的提升(尤其是数据量非常大的时候),但也依赖于计算机的CPU配置。

    Stream 能实现的功能,for 循环都能实现,只是 Stream 代码一般比较简洁,可读性强。但在某些情况下,使用 for 循环要比 Stream 要简洁代码逻辑清晰

    举个例子:

        private List<Order> orderList = Arrays.asList(
                new Order(103),
                new Order(106),
                new Order(107),
                new Order(104),
                new Order(102),
                new Order(103),
                new Order(102),
                new Order(101),
                new Order(104),
                new Order(102),
                new Order(105)
        );
        // 现根据 userId 设置 Order 对象的 name 属性
        
        // 使用 stream
        List<Order> newOrderList = orderList.stream()
                    .map(o -> userList.stream()
                            .filter(u -> u.getId() == o.getUserId())
                            .findFirst()
                            .map(u -> {
                                o.setUserName(u.getName());
                                return o;
                            })
                            .orElse(o))
                    .collect(Collectors.toList());
        newOrderList.stream().forEach(System.out::println);
    
        // 使用 for 循环
        for (Order o : orderList) {
            for (User u : userList) {
                if (o.getUserId() == u.getId()) {
                    o.setUserName(u.getName());
                    break;
                }
            }
        }
        orderList.stream().forEach(System.out::println);
    
    

    在这个例子中,使用 for 循环要比 使用 stream 干净利落的多,代码逻辑清晰简明,可读性也比 stream 好。

    相关文章

      网友评论

          本文标题:Java8 新特性(二)- Stream

          本文链接:https://www.haomeiwen.com/subject/mklfzctx.html