Lambda 表达式

2023-05-16

1. Lambda 表达式

1.1 通过接口传递代码

针对接口而非具体类型进行编程,可以降低程序的耦合性,提高灵活性,提高复用性。接口常被用于传递代码,比如,我们知道 File 有如下方法:

        public File[] listFiles(FilenameFilter filter)

listFiles需要的其实不是FilenameFilter对象,而是它包含的如下方法:

        boolean accept(File dir, String name);

或者说,listFiles希望接受一段方法代码作为参数,但没有办法直接传递这个方法代码本身,只能传递一个接口。

再如,类Collections中的很多方法都接受一个参数Comparator,比如:

        public static <T> void sort(List<T> list, Comparator<? super T> c)

它们需要的也不是Comparator对象,而是它包含的如下方法:

        int compare(T o1, T o2);

但是,没有办法直接传递方法,只能传递一个接口。

Callable和Runnable接口也用于传递任务代码。

通过接口传递行为代码,就要传递一个实现了该接口的实例对象,在之前,最简洁的方式是使用匿名内部类,比如:

        //列出当前目录下的所有扩展名为.txt的文件
        File f = new File(".");
        File[] files = f.listFiles(new FilenameFilter(){
            @Override
            public boolean accept(File dir, String name) {
                if(name.endsWith(".txt")){
                    return true;
                }
                return false;
            }
        });

将files按照文件名排序,代码为:

        Arrays.sort(files, new Comparator<File>() {
            @Override
            public int compare(File f1, File f2) {
                return f1.getName().compareTo(f2.getName());
            }
        });

1.2 Lambda 语法

Java 8 提供了一种新的紧凑的传递代码的语法:Lambda表达式。

        File f = new File(".");
        File[] files = f.listFiles((File dir, String name) -> {
            if(name.endsWith(".txt")) {
                return true;
            }
            return false;
        });

可以看出,相比匿名内部类,传递代码变得更为直观,不再有实现接口的模板代码,不再声明方法,也没有名字,而是直接给出了方法的实现代码。Lambda表达式由->分隔为两部分,前面是方法的参数,后面{}内是方法的代码上面的代码可以简化为:

        File[] files = f.listFiles((File dir, String name) -> {
            return name.endsWith(".txt");
        });

当主体代码只有一条语句的时候,括号和return语句也可以省略,上面的代码可以变为:

        File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));

注意:没有括号的时候,主体代码是一个表达式,这个表达式的值就是函数的返回值,结尾不能加分号,也不能加return语句。方法的参数类型声明也可以省略,上面的代码还可以继续简化为:

        File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));

之所以可以省略方法的参数类型,是因为Java可以自动推断出来,它知道listFiles接受的参数类型是FilenameFilter,这个接口只有一个方法accept,这个方法的两个参数类型分别是File和String。

排序的代码用Lambda表达式可以写为:

        Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

参数只有一个的时候,参数部分的括号可以省略。比如,File还有如下方法:

        public File[] listFiles(FileFilter filter)

FileFilter的定义为:

        public interface FileFilter {
            boolean accept(File pathname);
        }

使用FileFilter重写上面的列举文件的例子,代码可以为:

        File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));

与匿名内部类类似,Lambda表达式也可以访问定义在主体代码外部的变量,但对于局部变量,它也只能访问final类型的变量,与匿名内部类的区别是,它不要求变量声明为final,但变量事实上不能被重新赋值。

Java会将msg的值作为参数传递给Lambda表达式,为Lambda表达式建立一个副本,它的代码访问的是这个副本,而不是外部声明的msg变量。

Lambda表达式与匿名内部类很像,主要就是简化了语法,但它不是语法糖,内部实现不是内部类而是函数式接口。Java会为每个匿名内部类生成一个类,但Lambda表达式不会。

//普通方式1--------------------------------
class MThread implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable------1-------------");
    }
}

public class Test {
    public static void main(String[] args) {
        //方式1
        MThread mThread = new MThread();
        Thread thread = new Thread(mThread);
        thread.setName("线程1");
        thread.start();
    }

}

//匿名内部类 方式2--------------------------------
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable------2-------------");
            }
        }, "线程2").start();


//lambda表达式 方式3--------------------------------
        new Thread(() -> System.out.println("Runnable------3-------------"),"线程3").start();

1.3 函数式接口

Java 8 引入了函数式接口的概念,函数式接口也是接口,但只能有一个抽象方法,前面提及的接口都只有一个抽象方法,都是函数式接口。之所以强调是“抽象”方法,是因为Java 8中还允许定义静态方法和默认方法。Lambda表达式可以赋值给函数式接口,比如:

        FileFilter filter = path -> path.getName().endsWith(".txt");

        FilenameFilter fileNameFilter = (dir, name) -> name.endsWith(".txt");

        Comparator<File> comparator = (f1, f2) ->
                            f1.getName().compareTo(f2.getName());

        Runnable task = () -> System.out.println("hello world");

如果看这些接口的定义,会发现它们都有一个注解@FunctionalInterface,比如:

 @FunctionalInterface用于清晰地告知使用者这是一个函数式接口,不过,这个注解不是必需的,不加,只要只有一个抽象方法,也是函数式接口。但如果加了,而又定义了超过一个抽象方法,Java编译器会报错,这类似于我们之前介绍的Override注解。

1.4 预定义的函数式接口

Java 8定义了大量的预定义函数式接口,用于常见类型的代码传递,这些函数定义在包java.util.function下,主要接口如下:

 对于基本类型boolean、int、long和double,为避免装箱/拆箱,Java 8提供了一些专门的函数,比如,int相关的部分函数如下:

这些函数有什么用呢?它们被大量用于Java 8的函数式数据处理Stream相关的类中,即使不使用Stream,也可以在自己的代码中直接使用这些预定义的函数。

Function 示例

为便于举例,我们先定义一个简单的学生类Student,它有name和score两个属性,如下所示。

        static class Student {
            String name;
            double score;
        }

我们省略了构造方法和getter/setter方法。有一个学生列表:

        List<Student> students = Arrays.asList(new Student[] {
                new Student("zhangsan", 89d), new Student("lisi", 89d),
                new Student("wangwu", 98d) });

列表处理的一个常见需求是转换。

比如,给定一个学生列表,需要返回名称列表,或者将名称转换为大写返回,可以借助Function写一个通用的方法,如下所示:

        public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
            List<R> retList = new ArrayList<>(list.size());
            for(T e : list) {
                retList.add(mapper.apply(e));
            }
            return retList;
        }

根据学生列表返回名称列表的代码为:

        List<String> names = map(students, t -> t.getName());

将学生名称转换为大写的代码为:

        students = map(students, t -> new Student(
            t.getName().toUpperCase(), t.getScore()));

以上示例主要用于演示函数式接口的基本概念,实际中可以直接使用流API。

1.5 方法引用

Lambda 表达式经常用于调用对象的某个方法,比如:

        List<String> names = map(students, t -> t.getName());

这时,它可以进一步简化,如下所示:

        List<String> names = map(students, Student::getName);

Student::getName 这种写法是 Java 8 引入的一种新语法,称为方法引用。它是 Lambda 表达式的一种简写方法,由 ::分隔为两部分,前面是类名或变量名,后面是方法名。方法可以是实例方法,也可以是静态方法,但含义不同。

Lambda 方法引用举例

还是以Student为例,先增加一个静态方法:

        public static String getCollegeName(){
            return "Laoma School";
        }

对于静态方法,如下两条语句是等价的:

        1. Supplier<String> s = Student::getCollegeName;
        2. Supplier<String> s = () -> Student.getCollegeName();

它们的参数都是空,返回类型为String。

对于实例方法,它的第一个参数就是该类型的实例,比如,如下两条语句是等价的:

        1. Function<Student, String> f = Student::getName;
        2. Function<Student, String> f = (Student t) -> t.getName();

对于Student::setName,它是一个BiConsumer,即如下两条语句是等价的:

        1. BiConsumer<Student, String> c = Student::setName;
        2. BiConsumer<Student, String> c = (t, name) -> t.setName(name);

如果方法引用的第一部分是变量名,则相当于调用那个对象的方法。比如,假定t是一个Student类型的变量,则如下两条语句是等价的:

        1. Supplier<String> s = t::getName;
        2. Supplier<String> s = () -> t.getName();

下面两条语句也是等价的:

        1. Consumer<String> consumer = t::setName;
        2. Consumer<String> consumer = (name) -> t.setName(name);

对于构造方法,方法引用的语法是<类名>::new,如Student::new,即下面两条语句等价:

        1. BiFunction<String, Double, Student> s = (name, score) -> new Student(name, score);
        2. BiFunction<String, Double, Student> s = Student::new;

1.6 函数的复合

函数式接口和Lambda表达式还可用作方法的返回值,传递代码回调用者,将这两种用法结合起来,可以构造复合的函数,使程序简洁易读。

Comparator 中的复合方法

Comparator接口定义了如下静态方法:

        public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
                Function<? super T, ? extends U> keyExtractor) {
            Objects.requireNonNull(keyExtractor);
            return (Comparator<T> & Serializable)
                (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
        }

这个方法是什么意思呢?它用于构建一个Comparator,比如,在前面的例子中,对文件按照文件名排序的代码为:

        Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

使用comparing方法,代码可以简化为:

        Arrays.sort(files, Comparator.comparing(File::getName));

这样,代码的可读性就大大增强了。

Comparator还有很多默认方法,我们看两个:

        default Comparator<T> reversed() {
            return Collections.reverseOrder(this);
        }
        default Comparator<T> thenComparing(Comparator<? super T> other) {
            Objects.requireNonNull(other);
            return (Comparator<T> & Serializable) (c1, c2) -> {
                int res = compare(c1, c2);
                return (res ! = 0) ? res : other.compare(c1, c2);
            };
        }

reversed返回一个新的Comparator,按原排序逆序排。thenComparing也返回一个新的Comparator,在原排序认为两个元素排序相同的时候,使用传递的Comparator other进行比较。

看一个使用的例子,将学生列表按照分数倒序排(高分在前),分数一样的按照名字进行排序:

        students.sort(Comparator.comparing(Student::getScore)
                                  .reversed()
                                  .thenComparing(Student::getName));

2. 函数式数据处理:基本用法

Java 8 引入了一套新的类库,位于包 java.util.stream 下,称为 Stream API。

接口Stream类似于一个迭代器,但提供了更为丰富的操作,Stream API的主要操作就定义在该接口中。Java 8给Collection接口增加了两个默认方法,它们可以返回一个Stream,如下所示:

        default Stream<E> stream() {
            return StreamSupport.stream(spliterator(), false);
        }

        default Stream<E> parallelStream() {
            return StreamSupport.stream(spliterator(), true);
        }

stream()返回的是一个顺序流,parallelStream()返回的是一个并行流。顺序流就是由一个线程执行操作。而并行流背后可能有多个线程并行执行,与之前介绍的并发技术不同,使用并行流不需要显式管理线程,使用方法与顺序流是一样的。

2.1 基本示例

1)基本过滤

返回学生列表中90分以上的,传统上的代码一般是这样:

        List<Student> above90List = new ArrayList<>();
        for(Student t : students) {
            if(t.getScore() > 90) {
                above90List.add(t);
            }
        }

使用Stream API,代码可以这样:

        List<Student> above90List = students.stream()
                .filter(t->t.getScore()>90)
                .collect(Collectors.toList());

先通过stream()得到一个Stream对象,然后调用Stream上的方法,filter()过滤得到90分以上的,它的返回值依然是一个Stream,为了转换为List,调用了collect方法并传递了一个Collectors.toList(),表示将结果收集到一个List中。

2)基本转换

根据学生列表返回名称列表,传统上的代码一般是这样:

        List<String> nameList = new ArrayList<>(students.size());
        for(Student t : students) {
            nameList.add(t.getName());
        }

使用Stream API,代码可以这样:

        List<String> nameList = students.stream()
                .map(Student::getName).collect(Collectors.toList());

这里使用了 Stream 的 map 函数,它的参数是一个 Function 函数式接口,这里传递了方法引用。

3)基本的过滤和转换组合

返回90分以上的学生名称列表,传统上的代码一般是这样:

        List<String> nameList = new ArrayList<>();

        for(Student t : students) {
            if(t.getScore() > 90) {
                nameList.add(t.getName());
            }
        }

使用Stream API,可以将基本函数filter()和map()结合起来,代码可以这样:

        List<String> above90Names = students.stream()
                .filter(t->t.getScore()>90).map(Student::getName)
                .collect(Collectors.toList());

这种组合利用基本函数、声明式实现集合数据处理功能的编程风格,就是函数式数据处理。

调用 filter() 和 map() 不会执行任何实际的操作,它们只是在构建操作的流水线,调用 collect 才会触发实际的遍历执行,在一次遍历中完成过滤、转换以及收集结果的任务。所以不用担心它的性能问题。

像 filter 和 map 这种不实际触发执行、用于构建流水线、返回 Stream 的操作称为中间操作(intermediate operation),而像 collect 这种触发实际执行、返回具体结果的操作称为终端操作(terminal operation)

2.2 中间操作

除了 filter 和 map, Stream API 的中间操作还有 distinct、sorted、skip、limit、peek、mapToLong、mapToInt、mapToDouble、flatMap 等。

1)distinct

distinct返回一个新的Stream,过滤重复的元素,只留下唯一的元素,是否重复是根据equals方法来比较的,distinct可以与其他函数(如filter、map)结合使用。比如,返回字符串列表中长度小于3的字符串、转换为小写、只保留唯一的,代码可以为:

        List<String> list = Arrays.asList(new String[]{"abc", "def", "hello", "Abc"});

        List<String> retList = list.stream()
                .filter(s->s.length()<=3).map(String::toLowerCase).distinct()
                .collect(Collectors.toList());

虽然都是中间操作,但distinct与filter和map是不同的。filter和map都是无状态的,对于流中的每一个元素,处理都是独立的,处理后即交给流水线中的下一个操作;distinct不同,它是有状态的,在处理过程中,它需要在内部记录之前出现过的元素,如果已经出现过,即重复元素,它就会过滤掉,不传递给流水线中的下一个操作。对于顺序流,内部实现时,distinct操作会使用HashSet记录出现过的元素,如果流是有顺序的,需要保留顺序,会使用LinkedHashSet。

2)sorted

有两个sorted方法:

        Stream<T> sorted()
        Stream<T> sorted(Comparator<? super T> comparator)

它们都对流中的元素排序,都返回一个排序后的Stream。第一个方法假定元素实现了Comparable接口,第二个方法接受一个自定义的Comparator。比如,过滤得到90分以上的学生,然后按分数从高到低排序,分数一样的按名称排序,代码为:

        List<Student> studentList = students.stream()
                .filter(t -> t.getScore() > 90)
                .sorted(Comparator.comparing(Student::getScore)
                        .reversed()
                        .thenComparing(Student::getName))
                .collect(Collectors.toList());

这里,使用了Comparator的comparing、reversed和thenComparing构建了Comparator。

与distinct一样,sorted也是一个有状态的中间操作,在处理过程中,需要在内部记录出现过的元素。其不同是,每碰到流中的一个元素,distinct都能立即做出处理,要么过滤,要么马上传递给下一个操作;sorted需要先排序,为了排序,它需要先在内部数组中保存碰到的每一个元素,到流结尾时再对数组排序,然后再将排序后的元素逐个传递给流水线中的下一个操作。

3)skip / limit

它们的定义为:

        Stream<T> skip(long n)
        Stream<T> limit(long maxSize)

skip跳过流中的n个元素,如果流中元素不足n个,返回一个空流,limit限制流的长度为maxSize。比如,将学生列表按照分数排序,返回第3名到第5名,代码为:

        List<Student> list = students.stream()
                .sorted(Comparator.comparing(Student::getScore)
                        .reversed())
                .skip(2)
                .limit(3)
                .collect(Collectors.toList());

skip和limit都是有状态的中间操作。对前n个元素,skip的操作就是过滤,对后面的元素,skip就是传递给流水线中的下一个操作。limit的一个特点是:它不需要处理流中的所有元素,只要处理的元素个数达到maxSize,后面的元素就不需要处理了,这种可以提前结束的操作称为短路操作

skip 和 limit 只能根据元素数目进行操作,Java 9 增加了两个新方法,相当于更为通用的 skip 和 limit:

        //通用的skip,在谓词返回为true的情况下一直进行skip操作,直到某次返回false
        default Stream<T> dropWhile(Predicate<? super T> predicate)

        //通用的limit,在谓词返回为true的情况下一直接受,直到某次返回false
        default Stream<T> takeWhile(Predicate<? super T> predicate)

4)peek

peek的定义为:

        Stream<T> peek(Consumer<? super T> action)

它返回的流与之前的流是一样的,没有变化,但它提供了一个Consumer,会将流中的每一个元素传给该Consumer。这个方法的主要目的是支持调试,可以使用该方法观察在流水线中流转的元素,比如:

        List<String> above90Names = students.stream()
                .filter(t -> t.getScore() > 90)
                .peek(System.out::println)
                .map(Student::getName)
                .collect(Collectors.toList());

        System.out.println("above90Names结果:" + above90Names);

打印结果:
Student(name=liHua, score=100.0)
Student(name=wangwu, score=98.0)
above90Names结果:[liHua, wangwu]

5)mapToLong / mapToInt / mapToDouble

map函数接受的参数是一个Function<T, R>,为避免装箱/拆箱,提高性能,Stream 还有如下返回基本类型特定流的方法:

        DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
        IntStream mapToInt(ToIntFunction<? super T> mapper)
        LongStream mapToLong(ToLongFunction<? super T> mapper)

DoubleStream / IntStream / LongStream 是基本类型特定的流,有一些专门的更为高效的方法。比如,求学生列表的分数总和,代码为:

        double sum = students.stream().mapToDouble(Student::getScore).sum();

6)flatMap

flatMap 的定义为:

        <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

它接受一个函数 mapper,对流中的每一个元素,mapper 会将该元素转换为一个流 Stream,然后把新生成流的每一个元素传递给下一个操作。比如:

        List<String> lines = Arrays.asList(new String[]{"hello abc", "Janet  Ruby"});

        List<String> words = lines.stream()
                .flatMap(line -> Arrays.stream(line.split("\\s+")))
                .collect(Collectors.toList());

        System.out.println(words); //[hello, abc, Janet, Ruby]

这里的 mappe r将一行字符串按空白符分隔为了一个单词流,Arrays.stream可以将一个数组转换为一个流,输出为:

[hello, abc, Janet, Ruby]

可以看出,实际上,flatMap完成了一个1到n的映射。

2.3 终端操作

中间操作不触发实际的执行,返回值是Stream,而终端操作触发执行,返回一个具体的值,除了collect, Stream API的终端操作还有max、min、count、allMatch、anyMatch、noneMatch、findFirst、findAny、forEach、toArray、reduce 等。

1)max / min

max / min 的定义为:

        Optional<T> max(Comparator<? super T> comparator)
        Optional<T> min(Comparator<? super T> comparator)

它们返回流中的最大值/最小值,它们的返回值类型是Optional<T>,而不是T。

java.util.Optional是Java 8引入的一个新类,它是一个泛型容器类,内部只有一个类型为T的单一变量value,可能为null,也可能不为null。Optional有什么用呢?它用于准确地传递程序的语义,它清楚地表明,其代表的值可能为null,程序员应该进行适当的处理。

Optional定义了一些方法,比如:

        //value不为null时返回true
        public boolean isPresent()

        //返回实际的值,如果为null,抛出异常NoSuchElementException
        public T get()

        //如果value不为null,返回value,否则返回other
        public T orElse(T other)

        //构建一个空的Optional, value为null
        public static<T> Optional<T> empty()

        //构建一个非空的Optional, 参数value不能为null
        public static <T> Optional<T> of(T value)

        //构建一个Optional,参数value可以为null,也可以不为null
        public static <T> Optional<T> ofNullable(T value)

在max/min的例子中,通过声明返回值为Optional,我们可以知道具体的返回值不一定存在,这发生在流中不含任何元素的情况下。

看个简单的例子,返回分数最高的学生,代码为:

        //  这里,假定students不为空。
        Student student = students.stream()
                .max(Comparator.comparing(Student::getScore))
                .get();

2)count

count很简单,就是返回流中元素的个数。比如,统计大于90分的学生个数,代码为:

        long above90Count = students.stream().filter(t -> t.getScore() > 90).count();

3)allMatch/anyMatch/noneMatch

这几个函数都接受一个谓词Predicate,返回一个boolean值,用于判定流中的元素是否满足一定的条件。它们的区别是:

  • allMatch:只有在流中所有元素都满足条件的情况下才返回true。
  • anyMatch:只要流中有一个元素满足条件就返回true。
  • noneMatch:只有流中所有元素都不满足条件才返回true。

如果流为空,那么这几个函数的返回值都是true。

比如,判断是不是所有学生都及格了(不小于60分),代码可以为:

        boolean allPass = students.stream().allMatch(t -> t.getScore() >= 60);

这几个操作都是短路操作,不一定需要处理所有元素就能得出结果,比如,对于all-Match,只要有一个元素不满足条件,就能返回false。

4)findFirst / findAny

它们的定义为:

        Optional<T> findFirst()
        Optional<T> findAny()

它们的返回类型都是Optional,如果流为空,返回Optional.empty()。findFirst返回第一个元素,而findAny返回任一元素,它们都是短路操作。随便找一个不及格的学生,代码可以为:

        Optional<Student> failStudent = students.stream()
                .filter(t -> t.getScore() < 60)
                .findAny();
        if (failStudent.isPresent()) {
            //处理不及格的学生
           
        }

5)forEach

有两个 forEach 方法:

        void forEach(Consumer<? super T> action)
        void forEachOrdered(Consumer<? super T> action)

它们都接受一个Consumer,对流中的每一个元素,传递元素给Consumer。区别在于:在并行流中,forEach不保证处理的顺序,而forEachOrdered会保证按照流中元素的出现顺序进行处理。

比如,逐行打印大于90分的学生,代码可以为:

        students.stream().filter(t -> t.getScore() > 90).forEach(System.out::println);

再比如设置全部学生的分数为99分,代码可以为:

        students.forEach(data -> data.setScore(99d));

6)toArray

toArray 将流转换为数组,有两个方法:

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

不带参数的toArray返回的数组类型为Object[],这通常不是期望的结果,如果希望得到正确类型的数组,需要传递一个类型为IntFunction的generator。IntFunction的定义为:

        public interface IntFunction<R> {
            R apply(int value);
        }

generator接受的参数是流的元素个数,它应该返回对应大小的正确类型的数组。

比如,获取90分以上的学生数组,代码可以为:

        Student[] above90Arr = students.stream()
                .filter(t -> t.getScore() > 90)
                .toArray(Student[]::new);

Student[]::new 就是一个类型为 IntFunction<Student[]>的generator。

7)reduce

reduce代表归约或者叫折叠,它是 max/min/count 的更为通用的函数,将流中的元素归约为一个值。有三个reduce函数:

        Optional<T> reduce(BinaryOperator<T> accumulator);

        T reduce(T identity, BinaryOperator<T> accumulator);

        <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

第一个reduce函数基本等同于调用:

        boolean foundAny = false;
        T result = null;
        for(T element : this stream) {
            if(! foundAny) {
                foundAny = true;
                result = element;
            }
            else
                result = accumulator.apply(result, element);
        }
        return foundAny ? Optional.of(result) : Optional.empty();

比如,使用 reduce 函数求分数最高的学生,代码可以为:

        Student topStudent = students.stream()
                .reduce((accu, t) -> {
                    if (accu.getScore() >= t.getScore()) {
                        return accu;
                    } else {
                        return t;
                    }
                })
                .get();

第二个reduce函数多了一个identity参数,表示初始值,它基本等同于调用:

        T result = identity;
        for(T element : this stream)
            result = accumulator.apply(result, element)
        return result;

第一个和第二个reduce函数的返回类型只能是流中元素的类型,而第三个reduce函数更为通用,它的归约类型可以自定义,另外,它多了一个combiner参数。combiner用在并行流中,用于合并子线程的结果。对于顺序流,它基本等同于调用:

        U result = identity;
        for(T element : this stream)
            result = accumulator.apply(result, element)
        return result;

注意与第二个reduce函数相区分,它的结果类型不是T,而是U。

比如,使用reduce函数计算学生分数的和,代码可以为:

        double sumScore = students.stream().reduce(0d,
                (sum, t) -> sum += t.getScore(),
                (sum1, sum2) -> sum1 += sum2
        );

从以上可以看出,reduce 函数虽然更为通用,但比较费解,难以使用,一般情况下应该优先使用其他函数。collect函数比reduce函数更为通用、强大和易用。

2.4 构建流

前面我们主要使用的是Collection的stream方法,换做parallelStream方法,就会使用并行流,接口方法都是通用的。但并行流内部会使用多线程,线程个数一般与系统的CPU核数一样,以充分利用CPU的计算能力。

进一步来说,并行流内部会使用Java 7引入的fork/join框架,即处理由fork和join两个阶段组成,fork就是将要处理的数据拆分为小块,多线程按小块进行并行计算,join就是将小块的计算结果进行合并。使用并行流,不需要任何线程管理的代码,就能实现并行。

除了通过Collection接口的stream/parallelStream获取流,还有一些其他方式可以获取流。Arrays有一些stream方法,可以将数组或子数组转换为流,比如:

        public static IntStream stream(int[] array)
        
        public static DoubleStream stream(double[] array, int startInclusive, int endExclusive)
        
        public static <T> Stream<T> stream(T[] array)

输出当前目录下所有普通文件的名字,代码可以为:

        File[] files = new File(".").listFiles();
        Arrays.stream(files).filter(File::isFile).map(File::getName)
                .forEach(System.out::println);

Stream也有一些静态方法,可以构建流,比如:

        //返回一个空流
        public static<T> Stream<T> empty()

        //返回只包含一个元素t的流
        public static<T> Stream<T> of(T t)

        //返回包含多个元素values的流
        public static<T> Stream<T> of(T... values)

        //通过Supplier生成流,流的元素个数是无限的
        public static<T> Stream<T> generate(Supplier<T> s)

        //同样生成无限流,第一个元素为seed,第二个为f(seed),第三个为f(f(seed)),以此类推
        public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)

输出10个随机数,代码可以为:

        Stream.generate(()->Math.random()).limit(10).forEach(System.out::println);

输出100个递增的奇数,代码可以为:

        Stream.iterate(1, t->t+2).limit(100).forEach(System.out::println);

3. 函数式数据处理:强大方便的收集器

对于 collect 方法,前面只是演示了其最基本的应用,它还有很多强大的功能,比如,可以分组统计汇总,实现类似数据库查询语言 SQL 中的 group by 功能。

在上节中,过滤得到90分以上的学生列表,代码是这样的:

        List<Student> above90List = students.stream().filter(t->t.getScore()>90)
                .collect(Collectors.toList());

collect(Collectors.toList()) 背后的伪代码如下所示:

        List<T> container = new ArrayList<>();
        for(T t : data)
            container.add(t);
        return container;

3.1 容器收集器

toList 类似的容器收集器还有 toSettoCollectiontoMap

1)toSet

toSet 的使用与 toList 类似,只是它可以排重。

toList 背后的容器是 ArrayList,toSet 背后的容器是 HashSet。

2)toCollection

toCollection 是一个通用的容器收集器,可以用于任何 Collection 接口的实现类,它接受一个工厂方法 Supplier 作为参数。

比如,如果希望排重但又希望保留出现的顺序,可以使用 LinkedHashSet, Collector 可以这么创建:

        Collectors.toCollection(LinkedHashSet::new)

3)toMap

toMap将元素流转换为一个Map,我们知道,Map有键和值两部分,toMap至少需要两个函数参数,一个将元素转换为键,另一个将元素转换为值。

举例,将学生流转换为学生名称和分数的Map,代码可以为:

        Map<String, Double> nameScoreMap = students.stream().collect(
                Collectors.toMap(Student::getName, Student::getScore));

这里,Student::getName 是 keyMapper, Student::getScore 是 valueMapper。

再比如,假定Student的主键是id,希望转换学生流为学生id和学生对象的Map,代码可以为:

        Map<String, Student> byIdMap = students.stream().collect(
                Collectors.toMap(Student::getId, t -> t));

t->t是valueMapper,表示值就是元素本身。这个函数用得比较多,接口Function定义了一个静态函数identity表示它。也就是说,上面的代码可以替换为:

        Map<String, Student> byIdMap = students.stream().collect(
                Collectors.toMap(Student::getId, Function.identity()));

上面的toMap假定元素的键不能重复,如果有重复的,会抛出异常。

比如 希望得到字符串与其长度的Map,但由于包含重复字符串"abc",程序会抛出异常。这种情况下,我们希望的是程序忽略后面重复出现的元素,这时,可以使用另一个toMap函数(相比前面的toMap,它接受一个额外的参数mergeFunction,它用于处理冲突):

        Map<String, Integer> strLenMap = Stream.of("abc", "hello", "abc").collect(
                Collectors.toMap(Function.identity(), t->t.length(), (oldValue, value)->value));

有时,我们可能希望合并新值与旧值,比如一个联系人列表,对于相同的联系人,我们希望合并电话号码,mergeFunction可以定义为:

        BinaryOperator<String> mergeFunction = (oldPhone, phone)->oldPhone+", "+phone;

3.2 字符串收集器

除了将元素流收集到容器中,另一个常见的操作是收集为一个字符串。比如,获取所有的学生名称,用逗号连接起来,传统上代码看上去像这样:

        StringBuilder sb = new StringBuilder();
        for(Student t : students){
            if(sb.length()>0){
                sb.append(", ");
            }
            sb.append(t.getName());
        }
        return sb.toString();

针对这种常见的需求,Collectors提供了joining收集器,比如:

        public static Collector<CharSequence, ? , String> joining()

        public static Collector<CharSequence, ? , String> joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)

第一个就是简单地把元素连接起来,第二个支持一个分隔符,还可以给整个结果字符串加前缀和后缀,比如:

        String result = Stream.of("Janet", "Ruby", "Leo")
                .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(result); //[Janet, Ruby, Leo]

        String result1 = students.stream().map(t -> t.getName()).collect(Collectors.joining());
        System.out.println(result1); //liHuawangwulisizhangsanXiaoMei

        String result2 = students.stream().map(Student::getName).collect(Collectors.joining(",", "[", "]"));
        System.out.println(result2); //[liHua,wangwu,lisi,zhangsan,XiaoMei]

3.3 分组

分组类似于数据库查询语言SQL中的group by语句,它将元素流中的每个元素分到一个组,可以针对分组再进行处理和收集。

为便于举例,我们先修改下学生类Student,增加一个字段grade表示年级:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String name; //名字

    private double score; //分数

    private String grade; //年级

    public static String getCollegeName() {
        return "Laoma School";
    }
}

1)基本用法

将学生流按照年级进行分组,代码为:

        Map<String, List<Student>> groups = students.stream()
                .collect(Collectors.groupingBy(Student::getGrade));
        
        //System.out.println(groups); //{1=[Student(name=zhangsan, score=89.0, grade=1), Student(name=lisi, score=89.0, grade=1)], 2=[Student(name=liHua, score=100.0, grade=2), Student(name=wangwu, score=98.0, grade=2)], 3=[Student(name=XiaoMei, score=59.0, grade=3)]}

代码等同于:

        Map<String, List<Student>> groups = new HashMap<>();
        for(Student t : students) {
            String key = t.getGrade();
            List<Student> container = groups.get(key);
            if(container == null) {
                container = new ArrayList<>();
                groups.put(key, container);
            }
            container.add(t);
        }

2)分组计数、找最大/最小元素

为了便于使用 Collectors 中的方法,我们将其中的方法静态导入:

import static java.util.stream.Collectors.*;

统计每个年级的学生个数,代码可以为:

        Map<String, Long> gradeCountMap = students.stream().collect(
                groupingBy(Student::getGrade, counting()));

        //System.out.println(gradeCountMap); //{1=2, 2=2, 3=1}

统计一个单词流中每个单词的个数,按出现顺序排序,代码可以为:

        Map<String, Long> wordCountMap =
                Stream.of("hello", "world", "abc", "hello").collect(
                        groupingBy(Function.identity(), LinkedHashMap::new, counting()));
        
        //System.out.println(wordCountMap); //{hello=2, world=1, abc=1}

获取每个年级分数最高的一个学生,代码可以为:

        Map<String, Optional<Student>> topStudentMap = students.stream().collect(
                groupingBy(Student::getGrade,
                        maxBy(Comparator.comparing(Student::getScore))));
        
        //System.out.println(topStudentMap); //{1=Optional[Student(name=zhangsan, score=89.0, grade=1)], 2=Optional[Student(name=liHua, score=100.0, grade=2)], 3=Optional[Student(name=XiaoMei, score=59.0, grade=3)]}

需要说明的是,这个分组收集结果是Optional<Student>,而不是Student,这是因为maxBy处理的流可能是空流,但对我们的例子,这是不可能的。为了直接得到Student,可以使用Collectors的另一个收集器collectingAndThen,在得到Optional<Student>后调用Optional的get方法,如下所示:

        Map<String, Student> topStudentMap1 = students.stream().collect(
                groupingBy(Student::getGrade, collectingAndThen(
                        maxBy(Comparator.comparing(Student::getScore)), Optional::get)));

        //System.out.println(topStudentMap1); //{1=Student(name=zhangsan, score=89.0, grade=1), 2=Student(name=liHua, score=100.0, grade=2), 3=Student(name=XiaoMei, score=59.0, grade=3)}

3)分组数值统计

除了基本的分组计数,还经常需要进行一些分组数值统计,比如求学生分数的和、平均分、最高分、最低分等、针对int、long和double类型,Collectors提供了专门的收集器,如:

        //求平均值,int和long也有类似方法
        public static <T> Collector<T, ? , Double> averagingDouble(ToDoubleFunction<? super T> mapper)

        //求和,long和double也有类似方法
        public static <T> Collector<T, ? , Integer> summingInt(ToIntFunction<? super T> mapper)

        //求多种汇总信息,int和double也有类似方法
        //LongSummaryStatistics包括个数、最大值、最小值、和、平均值等多种信息
        public static <T> Collector<T, ? , LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)

比如,按年级统计学生分数信息,代码可以为:

        Map<String, DoubleSummaryStatistics> gradeScoreStat =
                students.stream().collect(groupingBy(Student::getGrade,
                        summarizingDouble(Student::getScore)));
        
        //System.out.println(gradeScoreStat); //{1=DoubleSummaryStatistics{count=2, sum=178.000000, min=89.000000, average=89.000000, max=89.000000}, 2=DoubleSummaryStatistics{count=2, sum=198.000000, min=98.000000, average=99.000000, max=100.000000}, 3=DoubleSummaryStatistics{count=1, sum=59.000000, min=59.000000, average=59.000000, max=59.000000}}

4)分组内的 map

对于每个分组内的元素,我们感兴趣的可能不是元素本身,而是它的某部分信息。在Stream API中,Stream有map方法,可以将元素进行转换,Collectors也为分组元素提供了函数mapping。

如,对学生按年级分组,得到学生名称列表,代码可以为:

        Map<String, List<String>> gradeNameMap =
                students.stream().collect(groupingBy(Student::getGrade,
                        mapping(Student::getName, toList())));
        
        //System.out.println(gradeNameMap); //{1=[zhangsan, lisi], 2=[liHua, wangwu], 3=[XiaoMei]}

5)分组结果处理(filter / sort / skip / limit)

对分组后的元素,我们可以计数,找最大/最小元素,计算一些数值特征,还可以转换(map)后再收集,那可不可以像Stream API一样,排序(sort)、过滤(filter)、限制返回元素(skip/limit)呢?Collector没有专门的收集器,但有一个通用的方法:

          public static<T, A, R, RR> Collector<T, A, RR> collectingAndThen( Collector<T, A, R> downstream, Function<R, RR> finisher)

将学生按年级分组,分组内的学生按照分数由高到低进行排序,利用这个方法,代码可以为:

先定义收集完再排序的方法:

    /**
     * 收集完再排序方法
     */
    public static <T> Collector<T, ?, List<T>> collectingAndSort(
            Collector<T, ?, List<T>> downstream, Comparator<? super T> comparator) {
        return Collectors.collectingAndThen(downstream, (r) -> {
            r.sort(comparator);
            return r;
        });
    }
        Map<String, List<Student>> gradeStudentMap = students.stream()
                .collect(groupingBy(Student::getGrade, collectingAndSort(toList(),
                        Comparator.comparing(Student::getScore).reversed())));

针对这个需求,也可以先对流进行排序,然后再分组。收集完再过滤,可以定义如下方法:

    /**
     * 收集完再过滤
     */
    public static <T> Collector<T, ?, List<T>> collectingAndFilter(
            Collector<T, ?, List<T>> downstream, Predicate<T> predicate) {
        return Collectors.collectingAndThen(downstream, (r) -> {
            return r.stream().filter(predicate).collect(Collectors.toList());
        });
    }

将学生按年级分组,分组后,每个分组只保留不及格的学生(低于60分)

        Map<String, List<Student>> gradeStudentMap = students.stream()
                .collect(groupingBy(Student::getGrade,
                        collectingAndFilter(toList(), t -> t.getScore() < 60)));

Java 9 中,Collectors 增加了一个新方法filtering,可以实现相同的功能:

        Map<String, List<Student>> gradeStudentMapdd = students.stream()
                .collect(groupingBy(Student::getGrade, filtering(t -> t.getScore() < 60, toList())));

收集完,只返回特定区间的结果,可以定义如下方法:

    /**
     * 收集完,只返回特定区间的结果
     */
    public static <T> Collector<T, ?, List<T>> collectingAndSkipLimit(
            Collector<T, ?, List<T>> downstream, long skip, long limit) {
        return Collectors.collectingAndThen(downstream, (r) -> {
            return r.stream().skip(skip).limit(limit)
                    .collect(Collectors.toList());
        });
    }

比如,将学生按年级分组,分组后,每个分组只保留前两名的学生,代码可以为:

        Map<String, List<Student>> gradeStudentMap2 = students.stream()
                .sorted(Comparator.comparing(Student::getScore).reversed())
                .collect(groupingBy(Student::getGrade,
                        collectingAndSkipLimit(toList(), 0, 2)));

6)分区

分组的一个特殊情况是分区,就是将流按 true/false 分为两个组,Collectors 有专门的分区函数:

        public static <T> Collector<T, ? , Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate)

        public static <T, D, A> Collector<T, ? , Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream)

比如,将学生按照是否及格(大于等于60分)分为两组,代码可以为:

        Map<Boolean, List<Student>> byPass = students.stream().collect(
                partitioningBy(t -> t.getScore() >= 60));
        
        //System.out.println(byPass); //{false=[Student(name=XiaoMei, score=59.0, grade=3)], true=[Student(name=zhangsan, score=89.0, grade=1), Student(name=lisi, score=89.0, grade=1), Student(name=liHua, score=100.0, grade=2), Student(name=wangwu, score=98.0, grade=2)]}

按是否及格分组后,计算每个分组的平均分,代码可以为:

        Map<Boolean, Double> avgScoreMap = students.stream().collect(
                partitioningBy(t -> t.getScore() >= 60, averagingDouble(Student::getScore)));

        //System.out.println(avgScoreMap); //{false=59.0, true=94.0}

7)多级分组

groupingBy和partitioningBy都可以接受一个下游收集器,对同一个分组或分区内的元素进行进一步收集,而下游收集器又可以是分组或分区,以构建多级分组。比如,按年级对学生分组,分组后,再按照是否及格对学生进行分区,代码可以为:

        Map<String, Map<Boolean, List<Student>>> multiGroup = students.stream()
                .collect(groupingBy(Student::getGrade,
                        partitioningBy(t->t.getScore()>=60)));

        //System.out.println(multiGroup); //{1={false=[], true=[Student(name=zhangsan, score=89.0, grade=1), Student(name=lisi, score=89.0, grade=1)]}, 2={false=[], true=[Student(name=liHua, score=100.0, grade=2), Student(name=wangwu, score=98.0, grade=2)]}, 3={false=[Student(name=XiaoMei, score=59.0, grade=3)], true=[]}}

代码附录:

第一部分,Student 类加 grade 属性之前:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String name; //名字

    private double score; //分数
    
    public static String getCollegeName() {
        return "Laoma School";
    }
}
    public static void main(String[] args) {

        //创建学习数组
        List<Student> students = Arrays.asList(new Student[]{
                new Student("zhangsan", 89d),
                new Student("lisi", 89d),
                new Student("liHua", 100d),
                new Student("XiaoMei", 59d),
                new Student("wangwu", 98d)});

        //---------------中间操作----------------------------------
        //1. 将学生列表按照分数倒序排(高分在前),分数一样的按照名字进行排序
        students.sort(Comparator.comparing(Student::getScore)
                .reversed()
                .thenComparing(Student::getName));

        //System.out.println(students); //[Student(name=liHua, score=100.0), Student(name=wangwu, score=98.0), Student(name=lisi, score=89.0), Student(name=zhangsan, score=89.0), Student(name=XiaoMei, score=59.0)]

        //2.返回学生列表中90分以上的
        List<Student> above90List = students.stream()
                .filter(t -> t.getScore() > 90).collect(Collectors.toList());

        //System.out.println(above90List); //[Student(name=liHua, score=100.0), Student(name=wangwu, score=98.0)]

        //3.根据学生列表返回名称列表
        List<String> nameList = students.stream()
                .map(Student::getName).collect(Collectors.toList());

        //System.out.println(nameList); //[liHua, wangwu, lisi, zhangsan, XiaoMei]


        //4. 返回字符串列表中长度小于3的字符串、转换为小写、只保留唯一的  (distinct:过滤)
        List<String> list = Arrays.asList(new String[]{"abc", "def", "hello", "Abc"});
        List<String> retList = list.stream()
                .filter(s -> s.length() <= 3)
                .map(String::toLowerCase)
                .distinct()
                .collect(Collectors.toList());

        //System.out.println(retList); //[abc, def]


        //5. 过滤得到90分以上的学生,然后按分数从高到低排序,分数一样的按名称排序
        List<Student> studentList = students.stream()
                .filter(t -> t.getScore() > 90)
                .sorted(Comparator.comparing(Student::getScore)
                        .reversed()
                        .thenComparing(Student::getName))
                .collect(Collectors.toList());

        //System.out.println(studentList); //[Student(name=liHua, score=100.0), Student(name=wangwu, score=98.0)]


        //6. 将学生列表按照分数排序,返回第3名到第5名
        List<Student> sList = students.stream()
                .sorted(Comparator.comparing(Student::getScore)
                        .reversed())
                .skip(2)
                .limit(3)
                .collect(Collectors.toList());

        //System.out.println(sList); //[Student(name=lisi, score=89.0), Student(name=zhangsan, score=89.0), Student(name=XiaoMei, score=59.0)]


        //7. 求学生列表的分数总和
        double studentSum = students.stream().mapToDouble(Student::getScore).sum();
        //System.out.println("学生列表的分数总和:"+studentSum); //学生列表的分数总和:435.0


        //---------------终端操作----------------------------------
        //1. 返回分数最高的学生  这里,假定students不为空。
        Student student = students.stream()
                .max(Comparator.comparing(Student::getScore))
                .get();
        System.out.println(student); //Student(name=liHua, score=100.0)

        Student student1 = students.stream()
                .max(Comparator.comparing(Student::getScore)
                        .reversed())
                .get();
        System.out.println(student1); //Student(name=XiaoMei, score=59.0)

        Student student2 = students.stream()
                .min(Comparator.comparing(Student::getScore))
                .get();
        System.out.println(student2); //Student(name=XiaoMei, score=59.0)


        //2. 统计大于90分的学生个数
        long above90Count = students.stream().filter(t -> t.getScore() > 90).count();
        //System.out.println(above90Count); //2

        //3. 判断是不是所有学生都及格了(不小于60分)
        boolean allPass = students.stream().allMatch(t -> t.getScore() >= 60);
        //System.out.println(allPass); //false


        //4. 随便找一个不及格的学生
        Optional<Student> failStudent = students.stream()
                .filter(t -> t.getScore() < 60)
                .findAny();
        if (failStudent.isPresent()) {
            //处理不及格的学生
            //System.out.println("这个人不及格:"+failStudent); //这个人不及格:Optional[Student(name=XiaoMei, score=59.0)]
        }

        //5. 逐行打印大于90分的学生
        //students.stream().filter(t -> t.getScore() > 90).forEach(System.out::println);

        //6. 获取90分以上的学生数组
        Student[] above90Arr = students.stream()
                .filter(t -> t.getScore() > 90)
                .toArray(Student[]::new);


        //---------------字符串收集器----------------------------------
        //9. 获取所有的学生名称,用逗号连接起来
        String result = Stream.of("Janet", "Ruby", "Leo")
                .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(result); //[Janet, Ruby, Leo]

        String result1 = students.stream().map(t -> t.getName()).collect(Collectors.joining());
        System.out.println(result1); //liHuawangwulisizhangsanXiaoMei

        String result2 = students.stream().map(Student::getName).collect(Collectors.joining(",", "[", "]"));
        System.out.println(result2); //[liHua,wangwu,lisi,zhangsan,XiaoMei]

    }

第二部分,加 grade 之后:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private String name; //名字

    private double score; //分数

    private String grade; //年级

    public static String getCollegeName() {
        return "Laoma School";
    }
}
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.*;

public class LambdaTest {

    public static void main(String[] args) {

        //创建学习数组
        List<Student> students = Arrays.asList(new Student[]{
                new Student("zhangsan", 89d, "1"),
                new Student("lisi", 89d, "1"),
                new Student("liHua", 100d, "2"),
                new Student("XiaoMei", 59d, "3"),
                new Student("wangwu", 98d, "2")});

        //1. 将学生流按照年级进行分组
        Map<String, List<Student>> groups = students.stream()
                .collect(Collectors.groupingBy(Student::getGrade));

        //System.out.println(groups); //{1=[Student(name=zhangsan, score=89.0, grade=1), Student(name=lisi, score=89.0, grade=1)], 2=[Student(name=liHua, score=100.0, grade=2), Student(name=wangwu, score=98.0, grade=2)], 3=[Student(name=XiaoMei, score=59.0, grade=3)]}


        //2. 统计每个年级的学生个数
        Map<String, Long> gradeCountMap = students.stream().collect(
                groupingBy(Student::getGrade, counting()));

        //System.out.println(gradeCountMap); //{1=2, 2=2, 3=1}

        //3.统计一个单词流中每个单词的个数,按出现顺序排序
        Map<String, Long> wordCountMap =
                Stream.of("hello", "world", "abc", "hello").collect(
                        groupingBy(Function.identity(), LinkedHashMap::new, counting()));

        //System.out.println(wordCountMap); //{hello=2, world=1, abc=1}

        //4. 获取每个年级分数最高的一个学生
        Map<String, Optional<Student>> topStudentMap = students.stream().collect(
                groupingBy(Student::getGrade,
                        maxBy(Comparator.comparing(Student::getScore))));

        //System.out.println(topStudentMap); //{1=Optional[Student(name=zhangsan, score=89.0, grade=1)], 2=Optional[Student(name=liHua, score=100.0, grade=2)], 3=Optional[Student(name=XiaoMei, score=59.0, grade=3)]}

        Map<String, Student> topStudentMap1 = students.stream().collect(
                groupingBy(Student::getGrade, collectingAndThen(
                        maxBy(Comparator.comparing(Student::getScore)), Optional::get)));

        //System.out.println(topStudentMap1); //{1=Student(name=zhangsan, score=89.0, grade=1), 2=Student(name=liHua, score=100.0, grade=2), 3=Student(name=XiaoMei, score=59.0, grade=3)}

        //5. 按年级统计学生分数信息
        Map<String, DoubleSummaryStatistics> gradeScoreStat =
                students.stream().collect(groupingBy(Student::getGrade,
                        summarizingDouble(Student::getScore)));

        //System.out.println(gradeScoreStat); //{1=DoubleSummaryStatistics{count=2, sum=178.000000, min=89.000000, average=89.000000, max=89.000000}, 2=DoubleSummaryStatistics{count=2, sum=198.000000, min=98.000000, average=99.000000, max=100.000000}, 3=DoubleSummaryStatistics{count=1, sum=59.000000, min=59.000000, average=59.000000, max=59.000000}}

        //6. 对学生按年级分组,得到学生名称列表
        Map<String, List<String>> gradeNameMap =
                students.stream().collect(groupingBy(Student::getGrade,
                        mapping(Student::getName, toList())));

        //System.out.println(gradeNameMap); //{1=[zhangsan, lisi], 2=[liHua, wangwu], 3=[XiaoMei]}

        //7. 将学生按年级分组,分组内的学生按照分数由高到低进行排序
        Map<String, List<Student>> gradeStudentMap = students.stream()
                .collect(groupingBy(Student::getGrade, collectingAndSort(toList(),
                        Comparator.comparing(Student::getScore).reversed())));

        //System.out.println(gradeStudentMap); //{1=[Student(name=zhangsan, score=89.0, grade=1), Student(name=lisi, score=89.0, grade=1)], 2=[Student(name=liHua, score=100.0, grade=2), Student(name=wangwu, score=98.0, grade=2)], 3=[Student(name=XiaoMei, score=59.0, grade=3)]}


        //8. 将学生按年级分组,分组后,每个分组只保留不及格的学生(低于60分)
        Map<String, List<Student>> gradeStudentMap1 = students.stream()
                .collect(groupingBy(Student::getGrade,
                        collectingAndFilter(toList(), t -> t.getScore() < 60)));

        //System.out.println(gradeStudentMap1); //{1=[], 2=[], 3=[Student(name=XiaoMei, score=59.0, grade=3)]}


        //9. 将学生按年级分组,分组后,每个分组只保留前两名的学生
        Map<String, List<Student>> gradeStudentMap2 = students.stream()
                .sorted(Comparator.comparing(Student::getScore).reversed())
                .collect(groupingBy(Student::getGrade,
                        collectingAndSkipLimit(toList(), 0, 2)));

        //System.out.println(gradeStudentMap2); //{1=[Student(name=zhangsan, score=89.0, grade=1), Student(name=lisi, score=89.0, grade=1)], 2=[Student(name=liHua, score=100.0, grade=2), Student(name=wangwu, score=98.0, grade=2)], 3=[Student(name=XiaoMei, score=59.0, grade=3)]}

        //10. 将学生按照是否及格(大于等于60分)分为两组
        Map<Boolean, List<Student>> byPass = students.stream().collect(
                partitioningBy(t -> t.getScore() >= 60));

        //System.out.println(byPass); //{false=[Student(name=XiaoMei, score=59.0, grade=3)], true=[Student(name=zhangsan, score=89.0, grade=1), Student(name=lisi, score=89.0, grade=1), Student(name=liHua, score=100.0, grade=2), Student(name=wangwu, score=98.0, grade=2)]}

        //11. 按是否及格分组后,计算每个分组的平均分
        Map<Boolean, Double> avgScoreMap = students.stream().collect(
                partitioningBy(t -> t.getScore() >= 60, averagingDouble(Student::getScore)));

        //System.out.println(avgScoreMap); //{false=59.0, true=94.0}

        //12. 按年级对学生分组,分组后,再按照是否及格对学生进行分区
        Map<String, Map<Boolean, List<Student>>> multiGroup = students.stream()
                .collect(groupingBy(Student::getGrade,
                        partitioningBy(t -> t.getScore() >= 60)));

        //System.out.println(multiGroup); //{1={false=[], true=[Student(name=zhangsan, score=89.0, grade=1), Student(name=lisi, score=89.0, grade=1)]}, 2={false=[], true=[Student(name=liHua, score=100.0, grade=2), Student(name=wangwu, score=98.0, grade=2)]}, 3={false=[Student(name=XiaoMei, score=59.0, grade=3)], true=[]}}


    }

    /**
     * 收集完再排序方法
     */
    public static <T> Collector<T, ?, List<T>> collectingAndSort(
            Collector<T, ?, List<T>> downstream, Comparator<? super T> comparator) {
        return Collectors.collectingAndThen(downstream, (r) -> {
            r.sort(comparator);
            return r;
        });
    }

    /**
     * 收集完再过滤
     */
    public static <T> Collector<T, ?, List<T>> collectingAndFilter(
            Collector<T, ?, List<T>> downstream, Predicate<T> predicate) {
        return Collectors.collectingAndThen(downstream, (r) -> {
            return r.stream().filter(predicate).collect(Collectors.toList());
        });
    }

    /**
     * 收集完,只返回特定区间的结果
     */
    public static <T> Collector<T, ?, List<T>> collectingAndSkipLimit(
            Collector<T, ?, List<T>> downstream, long skip, long limit) {
        return Collectors.collectingAndThen(downstream, (r) -> {
            return r.stream().skip(skip).limit(limit)
                    .collect(Collectors.toList());
        });
    }

}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Lambda 表达式 的相关文章

随机推荐

  • 【CMake】编译和链接静态库和动态库

    项目结构工作原理 配置项目编译库 项目结构 span class token builtin class name span include myClass h src CMakeLists txt myClass cpp CMakeLis
  • 字符串比较大小

    1 规则 1 如果 字符串1的第n位的ASCII码值 等于 字符串2的第n位的ASCII码值 则 继续比较下一位 2 如果 字符串1的第n位的ASCII码值 大于 字符串2的第n位的ASCII码值 则 输出结果 1 表示字符串1 gt 字符
  • 将本地jar添加到Maven本地仓库

    在Maven项目中 xff0c 如果需要引入自己的jar包 xff0c 需要将jar添加到本地Maven仓库 方法一 xff1a 假设将包htmlparser jar放入了项目下的lib目录中 xff1a gt project lib ht
  • UART的奇偶校验

    1 奇校验 当数据位中 1 的个数为奇数时 xff0c 校验位为 0 xff0c 否则为 1 2 偶校验 当数据位中 1 的个数为偶数时 xff0c 校验位为 0 xff0c 否则为 1
  • windows 关闭占用端口的进程

    1 netstat ano findstr yourPortNumber 2 taskkill PID typeyourPIDhere F
  • Linux TCP server/client例程

    1 服务器端 span class token macro property span class token directive hash span span class token directive keyword include s
  • Nvidia Jetson 平台 DeepStream-6.0.1 部署 YoloV5-6.0 实现目标检测

    项目介绍 xff1a 在 Jetson 平台上利用 DeepStream 处理多路视频源 xff0c 并实现自己训练的 YoloV5 模型的部署 文章目录 前言1 YoloV5 模型训练自己的数据集1 1 建立自己的数据集1 1 1 开始之
  • 软路由保姆级入门教程 一篇看懂软路由

    前言 amp nbsp amp nbsp 玩张大妈也一年多了 xff0c 软路由改装 刷机文章写了不少 xff0c 很早就打算写篇软路由入门文章 xff0c 但是一直没落实 xff0c 原因有二 xff1a 圈子里大佬众多 xff0c 基础
  • CMake入门02-CMake中的静态库

    CMake中的静态库 静态库 文件树 CMakeLists txt include static Hello h src Hello cpp main cpp 1 1 Hello h 声明了Hello类 xff0c Hello的方法是pri
  • C++:struct与class的区别

    xff08 1 xff09 C语言中struct与class的区别 xff1a a struct只作为一种复杂数据类型定义的结构体 xff0c 不能用于面向对象编程 xff1b b C语言没有class关键字 xff08 2 xff09 C
  • Rplidar A1使用并改为ROS中3D点云输出(PointCloud2)

    这里写自定义目录标题 Rplidar A1使用并改为ROS中3D点云输出Rplidar安装测试修改后完整代码测试 Rplidar A1使用并改为ROS中3D点云输出 3D激光雷达价格不菲 xff0c 在研究过程中 xff0c 可以尝试将2D
  • Spring/SpringBoot常用注解总结!

    以下内容皆为转载 xff0c 转载地址 xff1a 接近8000字的Spring SpringBoot常用注解总结 xff01 安排 xff01 1 64 SpringBootApplication 这里先单独拎出 64 SpringBoo
  • MobaXterm 复制粘贴快捷键

    1 复制 不用设置 xff0c MobaXTerm 里面选取内容就已经复制了 xff0c 如图 xff0c 白色的内容就已经成功复制了哈哈哈哈 xff0c 真方便 注 xff1a 如果不行 xff0c 看看是否是这里没有勾上 xff08 在
  • Apollo配置中心

    1 Apollo 介绍 Apollo xff08 阿波罗 xff09 是携程框架部门研发的分布式配置中心 xff0c 能够集中化管理应用不同环境 不同集群的配置 xff0c 配置修改后能够实时推送到应用端 xff0c 并且具备规范的权限 流
  • Linux 命令

    1 Linux Shell 获取本地当前时间或前一分钟时间 1 1 获取前一分钟时间 xff1a 1 xff09 默认格式 date d 34 1 minute ago 34 date d 34 1 minute ago 34 Thu Oc
  • powershell 遍历数据库表导出为csv

    Write Output 0 server 61 34 34 database 61 34 dbname 34 tablequery 61 34 SELECT schemas name as schemaName tables name a
  • 回顾一:lili-om编译及运行问题(process has died、Leaf size is too small 等)[Livox Horizon激光雷达]

    度过考试周和作业周 xff0c 终于有时间可以搞自己的东西啦 xff0c 半个月前学习的东西忘得差不多了 xff0c 现在凭借着仅剩的记忆回顾一下 x1f604 末尾有小惊喜 xff0c 嘿嘿 xff01 1 Ceres Solver1 1
  • EXCEl 时间戳转换为日期格式

    1 EXCEl 时间戳转换为日期格式 公式为 xff1a 61 TEXT A2 1000 43 8 3600 86400 43 70 365 43 19 34 yyyy mm dd hh mm ss 34 具体操作如下 xff1a A2 1
  • 代码分析工具 - SonarQube

    1 常见代码质量分析工具 SonarQube xff1a 可以分析27多种不同编程语言中的代码 xff0c 并帮助您提高性能和检测安全漏洞 它由SonarSource的团队开发 xff0c 对社区免费开源 SonarQube可以添加到您的C
  • Lambda 表达式

    1 Lambda 表达式 1 1 通过接口传递代码 针对接口而非具体类型进行编程 xff0c 可以降低程序的耦合性 xff0c 提高灵活性 xff0c 提高复用性 接口常被用于传递代码 xff0c 比如 xff0c 我们知道 File 有如