重拾Java(9)-Lambda表达式

以下内容是我对 Java 8 编程参考官方教程(第9版) 该书的读书笔记

一、简介

Lambda表达式(也称为闭包)是JDK8新增的功能,底层通过 invokedynamic 指令来生成匿名类来实现。Lambda表达式本质上是一个匿名方法,但这个方法不是独立执行的,而是用于实现由函数式接口定义的另一个方法。因此,Lambda表达式会导致生成一个匿名类。 函数式接口是仅包含一个抽象方法的接口。一般来说,这个方法指明了接口的目标用途。因此,函数式接口通常表示单个动作。例如,标准接口 Runnable 是一个函数式接口,因为它自定义了一个方法 run() 。因此,run() 定义了 Runnable 的动作。此外,函数式接口定义了 Lambda 表达式的目标类型。

二、Lambda表达式的基础知识

Lambda 表达式引入了一个新的语法元素和操作符,这个操作符是 -> 。它将 Lambda 表达式分为两个部分,左侧指定了 Lambda 表达式表达式需要的所有参数(如果不需要参数则使用空的参数列表),右侧指定了 Lambda 体,即 Lambda 表达式要执行的动作。 Java 定义了两种 Lambda 体。一种包含单独一个表达式,另一种包含一个代码块。 以下看几个例子:

    //不接收参数,直接返回 1.23
    ()->1.23

    //接受两个int类型参数,并返回这两个参数的乘积
    (int x,int y)->x*y;

    //接受两个参数,参数类型根据上下文推断出来,并返回参数的相加和
    (x,y)->x+y;

    //接受一个字符串参数,并将该字符串打印到控制到,无返回值
    (String param)->System.out.println(param);

    //接受一个推断类型的参数,并将该字符串打印到控制到,无返回值
    (param)->System.out.println(param);

    //接受两个参数,依次将字符串打印到控制到,无返回值
    (String name,int age)->{ System.out.println(name); System.out.println(age); }

当 Lambda 表达式需要参数时,需要在操作符左侧的参数列表中加以指定,可以显示指定参数的类型,但通常不需要这么做,因为很多时候参数的类型是可以推断出来的。

三、函数式接口

函数式接口是仅定义了一个抽象方法的接口,以下是函数式接口的一个例子:

public interface Filter {

    boolean filter();
    
}

如前所述,Lambda 表达式不是独立执行的,而是构成了一个函数式接口定义的抽象方法的实现,该函数式接口定义了它的目标类型。 首先,声明对函数式接口 Filter 的一个引用

    Filter filter

接下来,将一个 Lambda 表达式赋给该接口引用

    filter = ()-> true ;

当目标类型上下文中出现 Lambda 表达式时,会自动创建实现了函数式接口的一个类的实现,函数式接口声明的抽象方法的行为由 Lambda 表达式定义。当通过目标调用该方法时,就会执行 Lambda 表达式。因此,Lambda 表达式提供了一种将代码片段转换为对象的方法

四、使用示例

现在假设一个场景,需要对一批学生进行筛选,需要找出分别符合两个条件的学生:名字以“叶”开头,年龄大于18。

4.1、纯命令式的思路

如果使用传统的命令式的编程方法,一开始我们可能会这样写代码

public class Student {

    private String name;

    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Name: " + name + " Age: " + age;
    }
    
}
public class LambdaMain {

    public static void main(String[] args) {
        List<Student> studentList = getDataList();
        for (Student student : studentList) {
            if (student.getName().startsWith("叶")) {
                System.out.println(student);
            }
        }
        for (Student student : studentList) {
            if (student.getAge() > 18) {
                System.out.println(student);
            }
        }
    }

    private static List<Student> getDataList() {
        List<Student> studentList = new ArrayList<>();
        studentList.add(new Student("leavesC", 9));
        studentList.add(new Student("叶应是叶", 17));
        studentList.add(new Student("叶", 18));
        studentList.add(new Student("叶", 14));
        studentList.add(new Student("叶应是叶", 4));
        studentList.add(new Student("叶应是叶", 17));
        studentList.add(new Student("叶应是叶", 5));
        studentList.add(new Student("leavesC", 13));
        studentList.add(new Student("陈", 0));
        studentList.add(new Student("叶应是叶", 12));
        studentList.add(new Student("leavesC", 15));
        studentList.add(new Student("leavesC", 20));
        studentList.add(new Student("叶应是叶", 8));
        studentList.add(new Student("陈", 1));
        studentList.add(new Student("叶应是叶", 14));
        studentList.add(new Student("叶应是叶", 4));
        studentList.add(new Student("leavesC", 15));
        studentList.add(new Student("叶", 17));
        studentList.add(new Student("叶应是叶", 4));
        studentList.add(new Student("陈", 13));
        studentList.add(new Student("叶应是叶", 20));
        studentList.add(new Student("叶", 10));
        studentList.add(new Student("leavesC", 4));
        studentList.add(new Student("陈", 22));
        studentList.add(new Student("leavesC", 7));
        return studentList;
    }

}

毫无疑问这样是可以可以解决问题的,只是这样并不符合面向对象的思想

4.2、面向对象的编程方法

我们可以改为使用接口,具体的方法实现由不同的类来实现,由此得到一个典型的面向对象式的解决方案

    public interface Filter {

        boolean filter(Student student);

    }

    public static void main(String[] args) {
        List<Student> studentList = getDataList();
        List<Student> startsWithYeList = filterStudent(studentList, new Filter() {
            @Override
            public boolean filter(Student student) {
                return student.getName().startsWith("叶");
            }
        });
        List<Student> ageList = filterStudent(studentList, new Filter() {
            @Override
            public boolean filter(Student student) {
                return student.getAge() > 18;
            }
        });
    }

    private static List<Student> filterStudent(List<Student> studentList, Filter filter) {
        List<Student> students = new ArrayList<>();
        for (Student student : studentList) {
            if (filter.filter(student)) {
                students.add(student);
            }
        }
        return students;
    }

使用了面向对象的编程方法虽然使得逻辑结构更为清晰,具体的需求由特定的类来完成筛选,使用匿名类使代码也相对简洁了些,但这样依然会导致出现大量的重复代码。 重复的代码有:

        new Filter() {
            @Override
            public boolean filter(Student student) {
                return ***;
            }
        });

不重复的代码是:

student.getName().startsWith("叶");
student.getAge() > 18;

这就导致重复编写了两个基本一样的代码,我们需要思考的是,要怎么修改才能使得代码更好地应对变化的需求,而不是每个不同的需求都需要编写独立且基本逻辑相同的代码

4.3、使用 Lambda 表达式

若是改为 Lambda 表达式来完成需求,代码可以简化为

    public static void main(String[] args) {
        List<Student> studentList = getDataList();
        List<Student> startsWithYeList = filterStudent(studentList, (student) -> student.getName().startsWith("叶"));
        List<Student> ageList = filterStudent(studentList, (student) -> student.getAge() > 18);
    }

可以看到,在每个不同的需求中,仅仅只是传入了对应不同需求所需要的具体筛选操作,而不包含其它重复性代码,代码简化到了十分“干净”的程度,就连传入的参数的类型都给省略了,这是因为编译器可以根据上下文来推断参数类型 Filter 作为函数式接口只定义了单一抽象方法:boolean filter(Student student); 所以可以很容易地推断出其抽象方法需要的参数类型 此外,可以将不同的 Lambda 表达式赋给接口引用,就像普通的变量一样使用

    public static void main(String[] args) {
        List<Student> studentList = getDataList();

        Filter filter = (student) -> student.getName().startsWith("叶");
        List<Student> startsWithYeList = filterStudent(studentList, filter);

        filter = (student) -> student.getAge() > 18;
        List<Student> ageList = filterStudent(studentList, filter);
    }

五、块 Lambda 表达式

Lambda 表达式还有另外一种表达方式,其操作符右侧的代码可以由一个代码块构成,其中可以包含多条语句,就像其他一般的代码块一样。需要注意的是,块 Lambda 需要显式使用 return 语句来返回值。 例如,上一节中的 Lambda 表达式可以改写为如下形式:

    public static void main(String[] args) {
        List<Student> studentList = getDataList();
        //用花括号来包围 Lambda 体
        List<Student> startsWithYeList = filterStudent(studentList, student -> {
            String startString = "叶";
            return student.getName().startsWith(startString);
        });
        //返回一个随机的布尔值
        List<Student> ageList = filterStudent(studentList, student -> {
            Random random = new Random(3);
            return random.nextBoolean();
        });
    }

六、 Lambda 表达式访问变量

需要注意的是, Lambda 表达式对外部变量的操作权限会有一些不同,Lambda 表达式可以访问或修改其外层类的实例或静态变量的值,以及调用其外层类定义的方法。但是,当 Lambda 表达式使用其外层作用域内定义的局部变量时,该变量会被隐式地被声明为常量,所以在之后再次修改其值时就变得不合法了 此外,Lambda 表达式内部不能访问默认方法

    public interface Filter {

        boolean filter(Student student);
    
        //默认方法
        default void defaultFunctional(){

    }
    
    private static boolean flag1 = true;

    public static void main(String[] args) {
        boolean flag2 = true;
        Filter filter = student -> {
            //可以访问并修改静态变量
            flag1 = false;
            //可以访问但不能修改外部局部变量
            //在 Lambda 表达式内访问的外部局部变量会被隐式声明为final类型,因此只能访问而不能修改
            //flag2 = false;
            flag1 = flag2 && false;
            //不支持访问默认方法
            //defaultFunctional();
            return flag1;
        };
        //错误,已被隐式声明为常量了
        //flag2 = false;
    }
}

七、泛型函数式接口

与 Lambda 表达式关联的函数式接口可以是泛型,此次 Lambda 表达式的目标类型部分由声明函数式接口引用时指定的参数类型决定

上一节的 Filter 接口可以修改为

    public interface Filter<T> {

        boolean filter(T t);

    }

    public static void main(String[] args) {
    
        Filter<String> stringFilter = s -> s.length() > 20;
        
        Filter<Student> studentFilter = student -> student.getName().startsWith("叶");
        
    }

八、方法引用

Lambda 表达式使我们可以通过函数式接口来声明一个匿名方法,方法引用和 Lambda 表达式拥有类似的特性(都需要一个目标类型以及可以被转化为函数式接口的实例),我们并不需要为方法引用提供方法体,而是可以直接通过方法名称引用已有方法

8.1、静态方法的方法引用

静态方法的方法引用要使用如下语法进行声明,类名与方法名之间是有双冒号分隔开

 ClassName::methodName

被引用的方法的方法签名需要与函数式接口相同

public class LambdaMain {

    public interface Filter {

        boolean filter(Student student);

    }

    public static void main(String[] args) {

        Filter filter = LambdaMain::test;

        List<Student> studentList = filterStudent(new ArrayList<>(), filter);

    }

    private static boolean test(Student student) {
        return student.getAge() > 20;
    }

    private static List<Student> filterStudent(List<Student> studentList, Filter filter) {
        List<Student> students = new ArrayList<>();
        for (Student student : studentList) {
            if (filter.filter(student)) {
                students.add(student);
            }
        }
        return students;
    }

}

8.2、实例方法的方法引用

实例方法的方法引用用于传递对某个对象的实例方法的引用

public class Test {

    public Test() {

    }

    public boolean test(Student student) {
        return student.getAge() > 20;
    }

}


public class LambdaMain {

    public interface Filter {

        boolean filter(Student student);

    }


    public static void main(String[] args) {

        Test test = new Test();

        Filter filter = test::test;

        List<Student> studentList = filterStudent(new ArrayList<>(), filter);

    }


    private static List<Student> filterStudent(List<Student> studentList, Filter filter) {
        List<Student> students = new ArrayList<>();
        for (Student student : studentList) {
            if (filter.filter(student)) {
                students.add(student);
            }
        }
        return students;
    }

}

九、构造函数引用

构造函数引用与创建方法引用类似,可以创建构造函数的引用,然后把这个引用赋值给定义的方法与构造函数兼容的任何函数式接口的引用

public class LambdaMain {

    public interface Filter {

        MyClass filter(int value);

    }

    private static class MyClass {

        private int index;

        public MyClass(int index) {
            this.index = index;
        }

        public int getIndex() {
            return index;
        }
    }

    public static void main(String[] args) {
        //创建了对MyClass构造函数的引用
        Filter filter = MyClass::new;
        MyClass myClass = filter.filter(100);
        System.out.println(myClass.getIndex());
    }

}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏有趣的Python

慕课网-c语言入门-学习笔记

个人整理,学习自用。课程内容by慕课网。 c语言入门 C语言一经出现就以其功能丰富、表达能力强、灵活方便、应用面广等特点迅速在全世界普及和推广。C语言不但执行效...

4256
来自专栏闵开慧

Java盲点解析

1 堆栈区别     Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarr...

3156
来自专栏闻道于事

Java8Lambda表达式

使用 Lambda 表达式可以使代码变的更加简洁紧凑。有了Lambda表达式,java将开启函数式编程的大门。

642
来自专栏WindCoder

JavaScript字符串数组排序

思考路线:需要区分数字字符和非数字字符,故可知数字字符为此条件中的”特殊字符“,即特殊情况,需单独处理。数字字符的ASCII值为48-57。每次比较两个字符串(...

551
来自专栏糊一笑

面试题解法二:逆波兰表达式计算'1 + (5 - 2) * 3'

昨天发了一个面试题:关于一道面试题【字符串 ‘1 + (5 - 2) * 3’,怎么算出结果为10,’eval’除外】,受到了各位大大的指点,用一个比较简单的解...

3397
来自专栏一个会写诗的程序员的博客

第2章 Kotlin 语法基础第2章 Kotlin 语法基础

人与人之间通过语言来交流沟通,互相协作。人与计算机之间怎样“交流沟通”呢?答案是编程语言。一门语言有词、短语、句子、文章等,对应到编程语言中就是关键字、标识符、...

1032
来自专栏一“技”之长

Swift专题讲解二十三——高级运算符 原

        除了前边博客中介绍的基本运算符外,Swift中还支持更多高级运算符,也支持开发者进行运算符的自定义。Swift中的算符运算符有一个特点,其不会产...

431
来自专栏有趣的Python

0-浙大攻略计划-专业课-c语言入门(慕课网)

C语言入门 -> Linux C语言编程基本原理与实践 -> Linux C语言指针与内存 -> Linux C语言结构体

1272
来自专栏木东居士的专栏

通过源码分析 String、StringBuffer 和 StringBuilder

1493
来自专栏Java 源码分析

Java8新特性

1.HashMap 首先就是对 java 的 HashMap 进行了修改,以前是通过 hashCode 方法来判断他们的地址值是否一样 ,如果相同的话再使用 e...

3114

扫码关注云+社区