​Lambda 到底怎么“玩”

105 阅读12分钟

1.序

在这里插入图片描述

我们在把这张图拿出来,Jdk8最大的一个特点之一就是Lambda表达式,它支持JAVA也能进行简单的“函数式编程”。我原本以为用的很少。结果实习才发现,用的很多,因为它真的特别省劲,而且很简洁。公司其实代码的宗旨就是功能,可读性,简洁性。所以不会的同学赶紧来喵喵吧,不然也会像我一样面临不会改Lambda 代码的尴尬。

2.详解

2.1 Stream

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。 Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。 下面是Stream类

在这里插入图片描述

下面看一个例子证实为什么需要Stream,在Java7中如果实现排序和取值的话

public static void main(String[] args) {
        List<Student> allStudents = new ArrayList<>();
        List<Student> students = new ArrayList<>();
        for(Student s : allStudents){
            if(Student.pass  == s.getGrade()){
                students.add(s);
            }
        }

        Collections.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o2.getGrade() - o1.getGrade();
            }
        });
        
        List<Integer> studentIds = new ArrayList<>();
        for(Student s : allStudents){
            studentIds.add(s.getId());
        }
}

在Java 8 使用Stream ,代码简洁易读,而且使用并发模式,程序执行速度更快。

public void Test(){
        List<Student> allStudents = new ArrayList<>();
        List<Integer> studentIds = allStudents.parallelStream().
                filter(t -> t.getGrade() == Student.pass).
                sorted(Comparator.comparing(Student::getGrade).reversed()).
                map(Student::getId).
                collect(Collectors.toList());
    }

2.2.什么是lambda表达式?

它是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数。

λ表达式本质上是一个匿名方法。让我们来看下面这个例子:

public int add(int x,int y){
        return x + y;
    }
转为λ表达式之后就是这个样子:
    (int x,int y) -> x + y;
参数类型也可以省略,Java编译器会根据上下文推断出来:
    (x, y) -> x + y;//返回两数之和
    或者
(x, y) -> {return x + y}; //显式指明返回值

可见λ表达式有三部分组成:参数列表,箭头(-> ),以及一个表达式或者语句块。

下面这个例子里的λ表达式没有参数,也没有返回值(相当于一个方法接受0个参数,返回void,其实就是Runnable里run方法的一个实现): () -> { System.out.println("Hello Lambda!"); } 如果只有一个参数且可以被Java推断出来类型,那么参数列表的括号也可以省略: c -> {return c.size();}

2.3方法引用

方法引用是 lambda 表达式的一种特殊形式,即方法引用就是 Lambda 表达式(方法引用是 lambda 表达式的一种语法糖)。

方法引用用于获取一个已有方法的 Lambda 表达式形式。 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

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

上一节中我们也讲解了方法引用。 下面我们结合Lambda表达来讲解

        // 无参数情况
        Student student = ()-> new ArrayList<>();
        // 可替换为
        Student student = ArrayList::new;

        // 一个参数情况
        Student student = (String string) -> System.out.print(string);
        // 可替换为
        Student student = (System.out::println);

        // 两个参数情况
        Comparator c = (Student c1, Student c2) -> c1.getAge().compareTo(c2.getAge());
        // 可替换为
        Comparator c = (c1, c2) -> c1.getAge().compareTo(c2.getAge());
        // 进一步可替换为
        Comparator c = Comparator.comparing(Student::getAge);

2.4什么是流

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。

而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。

2.4.1流的构成

在这里插入图片描述 在这里插入图片描述

2.4.2中间操作

2.4.2.1filter

其定义为:Stream filter(Predicate<? super T> predicate),

  • filter接受一个谓词Predicate,我们可以通过这个谓词定义筛选条件,
  • 在介绍lambda表达式时我们介绍过Predicate是一个函数式接口,其包含一个test(T t)方法,该方法返回boolean。

现在我们希望从集合students中筛选出所有中国**大学的学生,那么我们可以通过filter来实现,并将筛选操作作为参数传递给filter:

List<Student> whuStudents = students.stream()
                                    .filter(student -> "中国**大学".equals(student.getSchool()))
                                    .collect(Collectors.toList());

2.4.2.2distinct

distinct操作类似于我们在写SQL语句时,添加的DISTINCT关键字,用于去重处理,distinct基于Object.equals(Object)实现,回到最开始的例子,假设我们希望筛选出所有不重复的偶数,那么可以添加distinct操作:

List<Integer> evens = nums.stream()
                        .filter(num -> num % 2 == 0).distinct()
                        .collect(Collectors.toList());

2.4.2.3limit

limit操作也类似于SQL语句中的LIMIT关键字,不过相对功能较弱,limit返回包含前n个元素的流,当集合大小小于n时,则返回实际长度,比如下面的例子返回前两个专业为计算机专业的学生:

List<Student> civilStudents = students.stream()
                                    .filter(student -> "计算机".equals(student.getMajor())).limit(2)
                                    .collect(Collectors.toList());

2.4.2.4sorted

说到limit,不得不提及一下另外一个流操作:sorted。该操作用于对流中元素进行排序, 简单的List排序

List<Integer> list = new ArrayList<>();
list.add(23);
list.add(74);
list.add(11);

----------------stream排序操作(默认ASC排序)

 List<Integer> collect = list.stream().sorted().collect(Collectors.toList());
System.out.println("list<Integer>元素正序:" + list);

打印结果:list元素正序:[11, 23, 74] ----------------stream倒序排序操作(DESC排序) reverseOrder();//反转排序

List<Integer> collect = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
System.out.println("list<Integer>元素倒序:" + collect );

sorted要求待比较的元素必须实现Comparable接口,如果没有实现也不要紧,我们可以将比较器作为参数传递给sorted(Comparator<? super T> comparator),比如我们希望筛选出专业为计算机的学生,并按年龄从小到大排序,筛选出年龄最小的两个学生,那么可以实现为:

List<Student> sortedCivilStudents = students.stream()
                                            .filter(student -> "计算机".equals(student.getMajor())).sorted((s1, s2) -> s1.getAge() - s2.getAge())
                                            .limit(2)
                                            .collect(Collectors.toList());

2.4.2.5skip

skip操作与limit操作相反,如同其字面意思一样,是跳过前n个元素,比如我们希望找出排序在2之后的计算机专业的学生,那么可以实现为:

List<Student> civilStudents = students.stream()
                                    .filter(student -> "计算机".equals(student.getMajor()))
                                    .skip(2)
                                    .collect(Collectors.toList());

通过skip,就会跳过前面两个元素,返回由后面所有元素构造的流,如果n大于满足条件的集合的长度,则会返回一个空的集合。 除了上面这类基础的map, java8还提供了 mapToDouble(ToDoubleFunction<? super T> mapper), mapToInt(ToIntFunction<? super T> mapper), mapToLong(ToLongFunction<? super T> mapper), 这些映射分别返回对应类型的流,java8为这些流设定了一些特殊的操作,比如我们希望计算专业为计算机科学学生的年龄之和,那么我们可以实现如下:

int totalAge = students.stream()
                    .filter(student -> "计算机科学".equals(student.getMajor()))
                    .mapToInt(Student::getAge).sum();

2.4.2.6map

举例说明,假设我们希望筛选出所有专业为计算机科学的学生姓名,那么我们可以在filter筛选的基础之上,通过map将学生实体映射成为学生姓名字符串,具体实现如下:

List<String> names = students.stream()
                            .filter(student -> "计算机科学".equals(student.getMajor()))
                            .map(Student::getName).collect(Collectors.toList());

通过将Student按照年龄直接映射为IntStream,我们可以直接调用提供的sum()方法来达到目的,此外使用这些数值流的好处还在于可以避免jvm装箱操作所带来的性能消耗。

2.4.2.7flatMap

通过以下示例讲解它与map区别

private static class Student {
        private int studentId;

        public Student(int studentId) {
            this.studentId = studentId;
        }

        @Override
        public String toString() {
            return "field=" + studentId;
        }
    }
    private static class StudentGroup {
        private List<Student> studentGroup = new ArrayList<>();

        public StudentGroup(Student... objList) {
            for (Student student : objList) {
                this.studentGroup.add(student);
            }
        }

        public List<Student> getStudentList() {
            return studentGroup;
        }
}

StudentGroup类中定义了一个Student类的列表 现在我们有一组StudentGroup对象

List<StudentGroup> groupList = Arrays.asList(
        new StudentGroup(new Student(1), new Student(2), new Student(3)),
        new StudentGroup(new Student(4), new Student(5), new Student(6)),
        new StudentGroup(new Student(7), new Student(8), new Student(9)),
        new StudentGroup(new Student(10))
);

需要将每个StudentGroup对象中的那些Klass类取出来,放到一个ArrayList里面,得到一个List。我们尝试着用map方法来实现。

List<List<Student>> result = groupList.stream()
                .map(it -> it.getStudentList())
                .collect(Collectors.toList());

哈,不成功,我们想要的结果是List,现在得到了 List<List>。当然,我们可以轻而易举的解决这个问题

List<Student> result2 = new ArrayList<>();
for (StudentGroup group : groupList) {
    for (Student student: group .getStudentList()) {
        result2.add(student);
    }
}

但是这种套了两层for循环的代码太丑陋了。面对这种需求,flatMap可以大展身手了

        List<Student> result3 = groupList.stream()
                .flatMap(it -> it.getStudentList().stream())
                .collect(Collectors.toList());

一行代码就实现了 stream api 的 flatMap方法接受一个lambda表达式函数, 函数的返回值必须也是一个stream类型,flatMap方法最终会把所有返回的stream合并,map方法做不到这一点,如果用map去实现,会变成这样一个东西

        List<Stream<Student>> result3 = groupList.stream()
                .map(it -> it.getStudentList().stream())
                .collect(Collectors.toList());

2.4.3 终端操作

终端操作是流式处理的最后一步,我们可以在终端操作中实现对流查找、归约等操作。

2.4.3.1 allMatch

allMatch用于检测是否全部都满足指定的参数行为,如果全部满足则返回true,例如我们希望检测是否所有的学生都已满18周岁,那么可以实现为:

boolean isAdult = students.stream().allMatch(student -> student.getAge() >= 18);

2.4.3.2anyMatch

anyMatch则是检测是否存在一个或多个满足指定的参数行为,如果满足则返回true,例如我们希望检测是否有来自中国**大学的学生,那么可以实现为:

boolean hasWhu = students.stream().anyMatch(student -> "中国**大学".equals(student.getSchool()));

2.4.3.3noneMathch

noneMatch用于检测是否不存在满足指定行为的元素,如果不存在则返回true,例如我们希望检测是否不存在专业为计算机科学的学生,可以实现如下:

boolean noneCs = students.stream().noneMatch(student -> "计算机科学".equals(student.getMajor()));

2.4.3.4findFirst

findFirst用于返回满足条件的第一个元素,比如我们希望选出专业为计算机的排在第一个学生,那么可以实现如下:

Optional optStu = students.stream().filter(student -> "计算机".equals(student.getMajor())).findFirst();

2.4.3.5 findAny

findAny相对于findFirst的区别在于,findAny不一定返回第一个,而是返回任意一个,比如我们希望返回任意一个专业为土木工程的学生,可以实现如下:

Optional optStu = students.stream().filter(student -> "土木工程".equals(student.getMajor())).findAny();

实际上对于顺序流式处理而言,findFirst和findAny返回的结果是一样的,至于为什么会这样设计,是因为在下一篇我们介绍的并行流式处理,当我们启用并行流式处理的时候,查找第一个元素往往会有很多限制,如果不是特别需求,在并行流式处理中使用findAny的性能要比findFirst好。

2.4.3.6计算集合元素的最大值、最小值、总和以及平均值

IntStream、LongStream 和 DoubleStream 等流的类中,有个非常有用的方法叫做 summaryStatistics() 。可以返回 IntSummaryStatistics、LongSummaryStatistics 或者 DoubleSummaryStatistic s,描述流中元素的各种摘要数据。在本例中,我们用这个方法来计算列表的最大值和最小值。它也有 getSum() 和 getAverage() 方法来获得列表的所有元素的总和及平均值。

//获取数字的个数、最小值、最大值、总和以及平均值

List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
IntSummaryStatistics stats = primes.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("Highest prime number in List : " + stats.getMax());
System.out.println("Lowest prime number in List : " + stats.getMin());
System.out.println("Sum of all prime numbers : " + stats.getSum());
System.out.println("Average of all prime numbers : " + stats.getAverage());

输出:

Highest prime number in List : 29
Lowest prime number in List : 2
Sum of all prime numbers : 129
Average of all prime numbers : 12.9

2.4.3.7reduce

reduce可实现根据指定的规则从Stream中生成一个值,比如之前提到的count,max和min方法是因为常用而被纳入标准库中。实际上,这些方法都是reduce的操作。

Stream.of(1, 2, 3).reduce(Integer::sum); Stream.of(1, 2, 3).reduce(0, (a, b) -> a + b); 以上两个方法都是对结果进行求和,不同的是第一个方法调用的是reduce的reduce((T, T) -> T)方法,而第二个调用的是reduce(T, (T, T) -> T)。其中第二个方法的第一个参数0,表示从第0个值开始操作。

2.4.3.8collect

收集方法,实现了很多归约操作,比如将流转换成集合和聚合元素等。

Stream.of(1, 2, 3).collect(Collectors.toList()); Stream.of(1, 2, 3).collect(Collectors.toSet());

除了以上的集合转换,还有类似joining字符串拼接的方法,具体可查看Collectors中的实现。

3.总结

其实Lambda表达式看的再多也不会让你使用的顺心应手,纸上得来终觉浅,绝知此事要躬行。一定要自己多实现,去尝试,这样才能熟练的掌握它,当你对它充满自信的时候,那么你的工作也会让你顺心。

4.个人推广

博客地址 blog.csdn.net/weixin_4156… 公众号 请关注 程序员面试之道

mp.weixin.qq.com/s/U2KvuYsRq…