当前位置 博文首页 > 口袋里的猫:JDK8中Stream使用解析

    口袋里的猫:JDK8中Stream使用解析

    作者:口袋里的猫 时间:2021-06-06 18:28

    JDK8中Stream使用解析

    现在谈及JDK8的新特新,已经说不上新了。本篇介绍的就是StreamLambda,说的Stream可不是JDK中的IO流,这里的Stream指的是处理集合的抽象概念『像流一样处理集合数据』。

    了解Stream前先认识一下Lambda

    函数式接口和Lambda

    先看一组简单的对比

    传统方式使用一个匿名内部类的写法

    new Thread(new Runnable() {
        @Override
        public void run() {
            // ...
        }
    }).start();
    

    换成Lambda的写法

    new Thread(() -> {
        // ...
    }).start();
    

    其实上面的写法就是简写了函数式接口匿名实现类

    配合Lambda,JDK8引入了一个新的定义叫做:函数式接口(Functional interfaces)

    函数式接口

    从概念上讲,有且仅有一个需要实现方法的接口称之为函数式接口

    看一个JDK给的一个函数式接口的源码

    @FunctionalInterface
    public interface Runnable {
        public abstract void run();
    }
    

    可以看到接口上面有一个@FunctionalInterface注释,功能大致和@Override类似

    不写@Override也能重写父类方法,该方法确实没有覆盖或实现了在超类型中声明的方法时编译器就会报错,主要是为了编译器可以验证识别代码编写的正确性。

    同样@FunctionalInterface也是这样,写到一个不是函数式接口的接口上面就会报错,即使不写@FunctionalInterface注释,编译器也会将满足函数式接口定义的任何接口视为函数式接口。

    写一个函数式接口加不加@FunctionalInterface注释,下面的接口都是函数式接口

    interface MyFunc {
      String show(Integer i);
    }
    

    Lambda表达式

    Lambda表达式就是为了简写函数式接口

    构成

    看一下Lambda的构成

    1. 括号里面的参数
    2. 箭头 ->
    3. 然后是身体
      • 它可以是单个表达式或java代码块。

    整体表现为 (...参数) -> {代码块}

    简写

    下面就是函数式接口的实现简写为Lambda的例子

    • 无参 - 无返回
    interface MyFunc1 {
        void func();
    }
    
    // 空实现
    MyFunc1 f11 = () -> { };
    // 只有一行语句
    MyFunc1 f12 = () -> {
        System.out.println(1);
        System.out.println(2);
    };
    // 只有一行语句
    MyFunc1 f13 = () -> {
        System.out.println(1);
    };
    // 只有一行语句可以省略 { }
    MyFunc1 f14 = () -> System.out.println(1);
    
    • 有参 - 无返回
    interface MyFunc2 {
        void func(String str);
    }
    
    // 函数体空实现
    MyFunc2 f21 = (str) -> { };
    // 单个参数可以省略 () 多个不可以省略
    MyFunc2 f22 = str -> System.out.println(str.length());
    
    • 无参 - 有返回
    interface MyFunc3 {
        int func();
    }
    
    // 返回值
    MyFunc3 f31 = () -> {return 1;};
    // 如果只有一个return 语句时可以直接写return 后面的表达式语句
    MyFunc3 f32 = () -> 1;
    
    • 有参 - 有返回
    interface MyFunc4 {
        int func(String str);
    }
    
    // 这里单个参数简写了{}
    MyFunc4 f41 = str -> {
        return str.length();
    };
    // 这里又简写了return
    MyFunc4 f42 = str -> str.length();
    // 这里直接使用了方法引用进行了简写 - 在文章后续章节有介绍到
    MyFunc4 f43 = String::length;
    

    这里可以总结出来简写规则

    上面写的Lambda表达式中参数都没有写参数类型(可以写参数类型的),so

    1. 小括号内参数的类型可以省略;
    2. 没有参数时小括号不能省略,小括号中有且仅有一个参数时,不能缺省括号
    3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号(三者省略都需要一起省略)。

    看到这里应该认识到了如何用Lambda简写函数式接口,那现在就进一步的认识一下JDK中Stream中对函数式接口的几种大类

    常用内置函数式接口

    上节说明了Lambda表达式就是为了简写函数式接口,为使用方便,JDK8提供了一些常用的函数式接口。最具代表性的为Supplier、Function、Consumer、Perdicate,这些函数式接口都在java.util.function包下。

    这些函数式接口都是泛型类型的,下面的源码都去除了default方法,只保留真正需要实现的方法。

    Function接口

    这是一个转换的接口。接口有参数、有返回值,传入T类型的数据,经过处理后,返回R类型的数据。『T和R都是泛型类型』可以简单的理解为这是一个加工工厂。

    @FunctionalInterface
    public interface Function<T, R> {
        R apply(T t);
    }
    

    使用实例:定义一个转换函数『将字符串转为数字,再平方』

    // 将字符串转为数字,再平方
    Function<String, Integer> strConvertToIntAndSquareFun = (str) -> {
        Integer value = Integer.valueOf(str);
        return value * value;
    };
    Integer result = strConvertToIntAndSquareFun.apply("4");
    System.out.println(result); // 16
    

    Supplier接口

    这是一个对外供给的接口。此接口无需参数,即可返回结果

    @FunctionalInterface
    public interface Supplier<T> {
        T get();
    }
    

    使用实例:定义一个函数返回“Tom”字符串

    // 供给接口,调用一次返回一个 ”tom“ 字符串
    Supplier<String> tomFun = () -> "tom";
    String tom = tomFun.get();
    System.out.println(tom); // tom
    

    Consumer接口

    这是一个消费的接口。此接口有参数,但是没有返回值

    @FunctionalInterface
    public interface Consumer<T> {
        void accept(T t);
    }	
    

    使用实例:定义一个函数传入数字,打印一行相应数量的A

    // 重复打印
    Consumer<Integer> printA = (n)->{
        for (int i = 0; i < n; i++) {
            System.out.print("A");
        }
        System.out.println();
    };
    printA.accept(5); // AAAAA
    

    Predicate接口

    这是一个断言的接口。此接口对输入的参数进行一系列的判断,返回一个Boolean值。

    @FunctionalInterface
    public interface Predicate<T> {
        boolean test(T t);	
    }
    

    使用实例:定义一个函数传入一个字符串,判断是否为A字母开头且Z字母结尾

    // 判断是否为`A`字母开头且`Z`字母结尾
    Predicate<String> strAStartAndZEnd = (str) -> {
        return str.startsWith("A") && str.endsWith("Z");
    };
    System.out.println(strAStartAndZEnd.test("AaaaZ")); // true 
    System.out.println(strAStartAndZEnd.test("Aaaaa")); // false
    System.out.println(strAStartAndZEnd.test("aaaaZ")); // false
    System.out.println(strAStartAndZEnd.test("aaaaa")); // false
    

    Supplier接口外Function、Consumer、Perdicate还有其他一堆默认方法可以用,比如Predicate接口包含了多种默认方法,用于处理复杂的判断逻辑(and, or);

    上面的使用方式都是正常简单的使用函数式接口,当函数式接口遇见了方法引用才真正发挥他的作用。

    方法引用

    方法引用的唯一存在的意义就是为了简写Lambda表达式。

    方法引用通过方法的名字来指向一个方法,可以使语言的构造更紧凑简洁,减少冗余代码。

    比如上面章节使用的

    MyFunc4 f43 = String::length; // 这个地方就用到了方法引用
    

    方法引用使用一对冒号 ::

    相当于将String类的实例方法length赋给MyFunc4接口

    public int length() {
        return value.length;
    }
    
    interface MyFunc4 {
        int func(String str);
    }
    

    这里可能有点问题:方法 int length()的返回值和int func(String str)相同,但是方法参数不同为什么也能正常赋值给MyFunc4

    可以理解为Java实例方法有一个隐藏的参数第一个参数this(类型为当前类)

    public class Student {
        public void show() {
            // ...
        }
        public void print(int a) {
            // ...
        }
    }
    

    实例方法show()print(int a)相当于

    public void show(String this);
    public void print(String this, int a);
    

    这样解释的通为什么MyFunc4 f43 = String::length;可以正常赋值。

    String::length;
    public int length() {
        return value.length;
    }
    
    // 相当于
    public int length(String str) {
        return str.length();
    }
    // 这样看length就和函数式接口MyFunc4的传参和返回值就相同了
    

    不只这一种方法引用详细分类如下

    方法引用分类

    类型 引用写法 Lambda表达式
    静态方法引用 ClassName::staticMethod (args) -> ClassName.staticMethod(args)
    对象方法引用 ClassName::instanceMethod (instance, args) -> instance.instanceMethod(args)
    实例方法引用 instance::instanceMethod (args) -> instance.instanceMethod(args)
    构建方法引用 ClassName::new (args) -> new ClassName(args)

    上面的方法就属于对象方法引用

    记住这个表格,不用刻意去记,使用Stream时会经常遇到

    有几种比较特殊的方法引用,一般来说原生类型如int不能做泛型类型,但是int[]可以

    IntFunction<int[]> arrFun = int[]::new;
    int[] arr = arrFun.apply(10); // 生成一个长度为10的数组
    

    这节结束算是把函数式接口,Lambda表达式,方法引用等概念串起来了。

    Optional工具

    Optional工具是一个容器对象,最主要的用途就是为了规避 NPE(空指针) 异常。构造方法是私有的,不能通过new来创建容器。是一个不可变对象,具体原理没什么可以介绍的,容器源码整个类没500行,本章节主要介绍使用。

    • 构造方法
    private Optional(T value) {
        // 传 null 会报空指针异常
        this.value = Objects.requireNonNull(value);
    }
    
    • 创建Optional的方法

    empyt返回一个包含null值的Optional容器

    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }
    

    of返回一个不包含null值的Optional容器,传null值报空指针异常

    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }
    

    ofNullable返回一个可能包含null值的Optional容器

    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }
    
    • 可以使用的Optional的方法

    ifPresent方法,参数是一个Consumer,当容器内的值不为null是执行Consumer

    Optional<Integer> opt = Optional.of(123);
    opt.ifPresent((x) -> {
    	System.out.println(opt);
    });
    // out: 123
    

    get方法,获取容器值,可能返回空

    orElse方法,当容器中值为null时,返回orElse方法的入参值

    public T orElse(T other) {
        return value != null ? value : other;
    }
    

    orElseGet方法,当容器中值为null时,执行入参Supplier并返回值

    public T orElseGet(Supplier<? extends T> other) {
        return value != null ? value : other.get();
    }
    
    • 常见用法
    // 当param为null时 返回空集合
    Optional.ofNullable(param).orElse(Collections.emptyList());
    Optional.ofNullable(param).orElseGet(() -> Collections.emptyList());
    

    orElseorElseGet的区别,orElseGet算是一个惰性求值的写法,当容器内的值不为null时Supplier不会执行。

    平常工作开发中,也是经常通过 orElse 来规避 NPE 异常。

    这方面不是很困难难主要是后续Stream有些方法需要会返回一个Optional一个容器对象。

    Stream

    Stream可以看作是一个高级版的迭代器。增强了Collection的,极大的简化了对集合的处理。

    想要使用Stream首先需要创建一个

    创建Stream流的方式

    // 方式1,数组转Stream
    Arrays.stream(arr);
    // 方式2,数组转Stream,看源码of就是方法1的包装
    Stream.of(arr);
    // 方式3,调用Collection接口的stream()方法
    List<String> list = new ArrayList<>();
    list.stream();
    

    有了Stream自然就少不了操作流

    常用Stream流方法

    大致可以把对Stream的操作大致分为两种类型中间操作终端操作

    • 中间操作是一个属于惰式的操作,也就是不会立即执行,每一次调用中间操作只会生成一个标记了新的Stream
    • 终端操作会触发实际计算,当终端操作执行时会把之前所有中间操作以管道的形式顺序执行,Stream是一次性的计算完会失效

    操作Stream会大量的使用Lambda表达式,也可以说它就是为函数式编程而生

    先提前认识一个终端操作forEach对流中每个元素执行一个操作,实现一个打印的效果

    // 打印流中的每一个元素
    Stream.of("jerry", "lisa", "moli", "tom", "Demi").forEach(str -> {
        System.out.println(str);
    });
    

    forEach的参数是一个Consumer可以用方法引用优化(静态方法引用),优化后的结果为

    Stream.of("jerry", "lisa", "moli", "tom", "Demi")
        .forEach(System.out::println);
    

    有这一个终端操作就可以向下介绍大量的中间操作了

    • 中间操作

    中间操作filter:过滤元素

    fileter方法参数是一个Predicate接口,表达式传入的参数是元素,返回true保留元素,false过滤掉元素

    过滤长度小于3的字符串,仅保留长度大于4的字符串

    Stream.of("jerry", "lisa", "moli", "tom", "Demi")
        // 过滤
        .filter(str -> str.length() > 3)
        .forEach(System.out::println);
    /*
    输出:
    jerry
    lisa
    moli
    Demi
    */
    

    中间操作limit:截断元素

    限制集合长度不能超过指定大小

    Stream.of("jerry", "lisa", "moli", "tom", "Demi")
        .limit(2)
        .forEach(System.out::println);
    /*
    输出:
    jerry
    lisa
    */
    

    中间操作skip:跳过元素(丢弃流的前n元素)

    // 丢弃前2个元素
    Stream.of("jerry", "lisa", "moli", "tom", "Demi")
        .skip(2)
        .forEach(System.out::println);
    /*
    输出:
    moli
    tom
    Demi
    */
    

    中间操作map:转换元素

    map传入的函数会被应用到每个元素上将其映射成一个新的元素

    // 为每一个元素加上 一个前缀 "name: "
    Stream.of("jerry", "lisa", "moli", "tom", "Demi")
        .map(str -> "name: " + str)
        .forEach(System.out::println);
    /*
    输出:
    name: jerry
    name: lisa
    name: moli
    name: tom
    name: Demi
    */
    

    中间操作peek:查看元素

    peek方法的存在主要是为了支持调试,方便查看元素流经管道中的某个点时的情况

    下面是一个JDK源码中给出的例子

    Stream.of("one", "two", "three", "four")
        // 第1次查看
        .peek(e -> System.out.println("第1次 value: " + e))
        // 过滤掉长度小于3的字符串
        .filter(e -> e.length() > 3)
        // 第2次查看
        .peek(e -> System.out.println("第2次 value: " + e))
        // 将流中剩下的字符串转为大写
        .map(String::toUpperCase)
        // 第3次查看
        .peek(e -> System.out.println("第3次 value: " + e))
        // 收集为List
        .collect(Collectors.toList());
    
    /*
    输出:
    第1次 value: one
    第1次 value: two
    第1次 value: three
    第2次 value: three
    第3次 value: THREE
    第1次 value: four
    第2次 value: four
    第3次 value: FOUR
    */
    

    mappeek有点相似,不同的是peek接收一个Consumer,而map接收一个Function

    当然了你非要采用peek修改数据也没人能限制的了

    public class User {
        public String name;
    
        public User(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "User{" +
                "name='" + name + '\'' +
                '}';
        }
    }
    
    Stream.of(new User("tom"), new User("jerry"))
        .peek(e -> {
            e.name = "US:" + e.name;
        })
        .forEach(System.out::println);
    /*
    输出:
    User{name='US:tom'}
    User{name='US:jerry'}
    */
    

    中间操作sorted:排序数据

    // 排序数据
    Stream.of(4, 2, 1, 3)
        // 默认是升序
        .sorted()
        .forEach(System.out::println);
    /*
    输出:
    1
    2
    3
    4
    */
    

    逆序排序

    // 排序数据
    Stream.of(4, 2, 1, 3)
        // 逆序
        .sorted(Comparator.reverseOrder())
        .forEach(System.out::println
    /*
    输出:
    4
    3
    2
    1
    */
    

    如果是对象如何排序,自定义Comparator,切记不要违反自反性,对称性,传递性原则

    public class User {
        public String name;
    
        public User(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "User{" +
                "name='" + name + '\'' +
                '}';
        }
    }
    
    // 名称长的排前面
    Stream.of(new User("tom"), new User("jerry"))
        .sorted((e1, e2) -> {
            return e2.name.length() - e1.name.length();
        })
        .forEach(System.out::println);
    /*
    输出:
    User{name='US:jerry'}
    User{name='US:tom'}
    */	
    

    中间操作distinct:去重

    注意:必须重写对应泛型的hashCode()和equals()方法

    
    Stream.of(2, 2, 4, 4, 3, 3, 100)
        .distinct()
        .forEach(System.out::println);
    /*
    输出:
    2
    4
    3
    100
    */	
    

    中间操作flatMap:平铺流

    返回一个流,该流由通过将提供的映射函数(flatMap传入的参数)应用于每个元素而生成的映射流的内容替换此流的每个元素,通俗易懂就是将原来的Stream中的所有元素都展开组成一个新的Stream

    
    List<Integer[]> arrList = new ArrayList<>();
    arrList.add(arr1);
    arrList.add(arr2);
    // 未使用
    arrList.stream()
        .forEach(e -> {
            System.out.println(Arrays.toString(e));
        });
    
    /*
    输出:
    [1, 2]
    [3, 4]
    */	
    
    // 平铺后
    arrList.stream()
        .flatMap(arr -> Stream.of(arr))
        .forEach(e -> {
            System.out.println(e);
        });
    
    /*
    输出:
    1
    2
    3
    4
    */	 	
    

    终端操作max,min,count:统计

    // 最大值
    Optional<Integer> maxOpt = Stream.of(2, 4, 3, 100)
        .max(Comparator.comparing(e -> e));
    System.out.println(maxOpt.get()); // 100
    
    // 最小值
    Optional<Integer> minOpt = Stream.of(2, 4, 3, 100)
        .min(Comparator.comparing(Function.identity()));
    System.out.println(minOpt.get()); // 2
    
    // 数量
    long count = Stream.of("one", "two", "three", "four")
        .count();
    System.out.println(count); // 4
    

    上面例子中有一个点需要注意一下Function.identity()相当于 e -> e

    看源码就可以看出来

    static <T> Function<T, T> identity() {
        return t -> t;
    }
    

    终端操作findAny:返回任意一个元素

    Optional<String> anyOpt = Stream.of("one", "two", "three", "four")
        .findAny();
    System.out.println(anyOpt.orElse(""));
    /*
    输出:
    one
    */	
    

    终端操作findFirst:返回第一个元素

    Optional<String> firstOpt = Stream.of("one", "two", "three", "four")
        .findFirst();
    
    System.out.println(firstOpt.orElse(""));
    /*
    输出:
    one
    */	
    

    返回的Optional容器在上面介绍过了,一般配置orElse使用,原因就在于findAnyfindFirst可能返回空空容器,调用get可能会抛空指针异常

    终端操作allMatch,anyMatch:匹配

    // 是否全部为 one 字符串
    boolean allIsOne = Stream.of("one", "two", "three", "four")
        .allMatch(str -> Objects.equals("one", str));
    System.out.println(allIsOne); // false
    
    allIsOne = Stream.of("one", "one", "one", "one")
        .allMatch(str -> Objects.equals("one", str));
    System.out.println(allIsOne); // true
    
    // 是否包含 one 字符串
    boolean hasOne = Stream.of("one", "two", "three", "four")
        .anyMatch(str -> Objects.equals("one", str));
    System.out.println(hasOne); // true
    
    hasOne = Stream.of("two", "three", "four")
        .anyMatch(str -> Objects.equals("one", str));
    System.out.println(hasOne); // false
    

    上面仅仅介绍了一个forEach终端操作,但是业务开发中更多的是对处理的数据进行收集起来,如下面的一个例子将元素收集为一个List集合

    终端操作collect:收集元素到集合

    collect高级使用方法很复杂,常用的用法使用Collectors工具类

    • 收集成List
    List<String> list = Stream.of("one", "two", "three", "four")
        .collect(Collectors.toList());
    System.out.println(list);
    /*
    输出:
    [one, two, three, four]
    */	
    
    • 收集成Set『收集后有去除的效果,结果集乱序』
    Set<String> set = Stream.of("one", "one", "two", "three", "four")
        .collect(Collectors.toSet());
    System.out.println(set);
    /*
    输出:
    [four, one, two, three]
    */	
    
    • 字符串拼接
    String str1 = Stream.of("one", "two", "three", "four")
        .collect(Collectors.joining());
    System.out.println(str1); // onetwothreefour
    String str2 = Stream.of("one", "two", "three", "four")
        .collect(Collectors.joining(", "));
    System.out.println(str2); // one, two, three, four
    
    • 收集成Map
    // 使用Lombok插件
    @Data
    @AllArgsConstructor
    public class User {
        public Integer id;
        public String name;
    }
    
    Map<Integer, User> map = Stream.of(new User(1, "tom"), new User(2, "jerry"))
        .collect(Collectors.toMap(User::getId, Function.identity(), (k1, k2) -> k1));
    System.out.println(map);
    /*
    输出:
    {
        1=User(id=1, name=tom), 
        2=User(id=2, name=jerry)
    }
    */	
    
    

    toMap常用的方法签名

    public static <T, K, U>
        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);
    }
    /*
    keyMapper:Key 的映射函数
    valueMapper:Value 的映射函数
    mergeFunction:当 Key 冲突时,调用的合并方法
    */
    
    • 数据分组
    @Data
    @AllArgsConstructor
    class User {
        public Integer id;
        public String name;
    }
    Map<String, List<User>> map = Stream.of(
        new User(1, "tom"), new User(2, "jerry"),
        new User(3, "moli"), new User(4, "lisa")
    ).collect(Collectors.groupingBy(u -> {
        if (u.id % 2 == 0) {
            return "奇";
        }
        return "偶";
    }));
    System.out.println(map);
    /*
    输出:
    {
        偶=[User(id=1, name=tom), User(id=3, name=moli)], 
        奇=[User(id=2, name=jerry), User(id=4, name=lisa)]
    }
    */	
    
    

    分组后value 是一个集合,groupingBy分组还有一个参数可以指定下级收集器,后续例子中有使用到

    Steam例

    下面例子用到的基础数据,如有例子特例会在例子中单独补充

    List<Student> studentList = new ArrayList<>();
    studentList.add(new Student(1, "tom",    19, "男", "软工"));
    studentList.add(new Student(2, "lisa",   15, "女", "软工"));
    studentList.add(new Student(3, "Ada",    16, "女", "软工"));
    studentList.add(new Student(4, "Dora",   14, "女", "计科"));
    studentList.add(new Student(5, "Bob",    20, "男", "软工"));
    studentList.add(new Student(6, "Farrah", 15, "女", "计科"));
    studentList.add(new Student(7, "Helen",  13, "女", "软工"));
    studentList.add(new Student(8, "jerry",  12, "男", "计科"));
    studentList.add(new Student(9, "Adam",   20, "男", "计科"));
    

    例1:封装一个分页函数

    /**
    * 分页方法
    *
    * @param list     要分页的数据
    * @param pageNo   当前页
    * @param pageSize 页大小
    */
    public static <T> List<T> page(Collection<T> list, long pageNo, long pageSize) {
        if (Objects.isNull(list) || list.isEmpty()) {
            return Collections.emptyList();
        }
        return list.stream()
            .skip((pageNo - 1) * pageSize)
            .limit(pageSize)
            .collect(Collectors.toList());
    }
    
    List<Student> pageData = page(studentList, 1, 3);
    System.out.println(pageData);
    /*
    输出:
    [
      Student(id=1, name=tom, age=19, sex=男, className=软工), 
      Student(id=2, name=lisa, age=15, sex=女, className=软工), 
      Student(id=3, name=Ada, age=16, sex=女, className=软工)
    ]
    */
    

    例2:获取软工班全部的人员id

    List<Integer> idList = studentList.stream()
        .filter(e -> Objects.equals(e.getClassName(), "软工"))
        .map(Student::getId)
        .collect(Collectors.toList());
    System.out.println(idList);
    /*
    输出:
    [1, 2, 3, 5, 7]
    */
    

    例3:收集每个班级中的人员名称列表

    Map<String, List<String>> map = studentList.stream()
            .collect(Collectors.groupingBy(
                    Student::getClassName,
                    Collectors.mapping(Student::getName, Collectors.toList())
            ));
    System.out.println(map);
    /*
    输出:
    {
      计科=[Dora, Farrah, jerry, Adam], 
      软工=[tom, lisa, Ada, Bob, Helen]
    }
    */
    

    例4:统计每个班级中的人员个数

    Map<String, Long> map = studentList.stream()
        .collect(Collectors.groupingBy(
            Student::getClassName,
            Collectors.mapping(Function.identity(), Collectors.counting())
        ));
    System.out.println(map);
    /*
    输出:
    {
      计科=4, 
      软工=5
    }
    */
    

    例5:获取全部女生的名称

    List<String> allFemaleNameList = studentList.stream()
        .filter(stu -> Objects.equals("女", stu.getSex()))
        .map(Student::getName)
        .collect(Collectors.toList());
    System.out.println(allFemaleNameList);
    /*
    输出:
    [lisa, Ada, Dora, Farrah, Helen]
    */
    

    例6:依照年龄排序

    // 年龄升序排序
    List<Student> stuList1 = studentList.stream()
        // 升序
        .sorted(Comparator.comparingInt(Student::getAge))
        .collect(Collectors.toList());
    System.out.println(stuList1);
    /*
    输出:
    [
    Student(id=8, name=jerry, age=12, sex=男, className=计科), 
    Student(id=7, name=Helen, age=13, sex=女, className=软工), 
    Student(id=4, name=Dora, age=14, sex=女, className=计科), 
    Student(id=2, name=lisa, age=15, sex=女, className=软工), 
    Student(id=6, name=Farrah, age=15, sex=女, className=计科), 
    Student(id=3, name=Ada, age=16, sex=女, className=软工), 
    Student(id=1, name=tom, age=19, sex=男, className=软工), 
    Student(id=5, name=Bob, age=20, sex=男, className=软工), 
    Student(id=9, name=Adam, age=20, sex=男, className=计科)
    ]
    */
    
    // 年龄降序排序
    List<Student> stuList2 = studentList.stream()
        // 降序
        .sorted(Comparator.comparingInt(Student::getAge).reversed())
        .collect(Collectors.toList());
    System.out.println(stuList2);
    /*
    输出:
    [
    Student(id=5, name=Bob, age=20, sex=男, className=软工), 
    Student(id=9, name=Adam, age=20, sex=男, className=计科), 
    Student(id=1, name=tom, age=19, sex=男, className=软工), 
    Student(id=3, name=Ada, age=16, sex=女, className=软工), 
    Student(id=2, name=lisa, age=15, sex=女, className=软工), 
    Student(id=6, name=Farrah, age=15, sex=女, className=计科), 
    Student(id=4, name=Dora, age=14, sex=女, className=计科), 
    Student(id=7, name=Helen, age=13, sex=女, className=软工), 
    Student(id=8, name=jerry, age=12, sex=男, className=计科)
    ]
    */
    

    例7:分班级依照年龄排序

    该例中和例3类似的处理,都使用到了downstream下游 - 收集器

    Map<String, List<Student>> map = studentList.stream()
            .collect(
                    Collectors.groupingBy(
                            Student::getClassName,
                            Collectors.collectingAndThen(Collectors.toList(), arr -> {
                                return arr.stream()
                                        .sorted(Comparator.comparingInt(Student::getAge))
                                        .collect(Collectors.toList());
                            })
                    )
            );
    /*
    输出:
    {
      计科 =[
        Student(id = 8, name = jerry, age = 12, sex = 男, className = 计科), 
        Student(id = 4, name = Dora, age = 14, sex = 女, className = 计科), 
        Student(id = 6, name = Farrah, age = 15, sex = 女, className = 计科), 
        Student(id = 9, name = Adam, age = 20, sex = 男, className = 计科)
      ],
      软工 =[
        Student(id = 7, name = Helen, age = 13, sex = 女, className = 软工), 
        Student(id = 2, name = lisa, age = 15, sex = 女, className = 软工), 
        Student(id = 3, name = Ada, age = 16, sex = 女, className = 软工), 
        Student(id = 1, name = tom, age = 19, sex = 男, className = 软工), 
        Student(id = 5, name = Bob, age = 20, sex = 男, className = 软工)
      ]
    }
    */
    

    本例中使用到的downstream的方式更为通用,可以实现绝大多数的功能,例3中的方法JDK提供的简写方式

    下面是用collectingAndThen的方式实现和例3相同的功能

    Map<String, Long> map = studentList.stream()
            .collect(
                    Collectors.groupingBy(
                            Student::getClassName,
                            Collectors.collectingAndThen(Collectors.toList(), arr -> {
                                return (long) arr.size();
                            })
                    )
            );
    /*
    输出:
    {
      计科=4, 
      软工=5
    }
    */
    

    例8:将数据转为ID和Name对应的数据结构Map

    Map<Integer, String> map = studentList.stream()
        .collect(Collectors.toMap(Student::getId, Student::getName));
    System.out.println(map);
    /*
    输出:
    {
      1=tom, 
      2=lisa, 
      3=Ada, 
      4=Dora, 
      5=Bob, 
      6=Farrah, 
      7=Helen, 
      8=jerry, 
      9=Adam
    }
    */
    
    • 情况1

    上面代码,在现有的数据下正常运行,当添加多添加一条数据

    studentList.add(new Student(9, "Adam - 2", 20, "男", "计科"));
    

    这个时候id为9的数据有两条了,这时候再运行上面的代码就会出现Duplicate key Adam

    也就是说调用toMap时,假设其中存在重复的key,如果不做任何处理,会抛异常

    解决异常就要引入toMap方法的第3个参数mergeFunction,函数式接口方法签名如下

    R apply(T t, U u);
    

    代码修改后如下

    Map<Integer, String> map = studentList.stream()
        .collect(Collectors.toMap(Student::getId, Student::getName, (v1, v2) -> {
            System.out.println("value1: " + v1);
            System.out.println("value2: " + v2);
            return v1;
        }));
    /*
    输出:
    value1: Adam
    value2: Adam - 2
    {1=tom, 2=lisa, 3=Ada, 4=Dora, 5=Bob, 6=Farrah, 7=Helen, 8=jerry, 9=Adam}
    */
    

    可以看出来mergeFunction 参数v1为原值,v2为新值

    日常开发中是必须要考虑第3参数的mergeFunction,一般采用策略如下

    // 参数意义: o 为原值(old),n 为新值(new)
    studentList.stream()
        // 保留策略
        .collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> o));
    
    
    studentList.stream()
        // 覆盖策略
        .collect(Collectors.toMap(Student::getId, Student::getName, (o, n) -> n));
    
    
    下一篇:没有了