重拾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 条评论
登录 后参与评论

相关文章

来自专栏linux驱动个人学习

高通Audio中ASOC的machine驱动

ASoC被分为Machine、Platform和Codec三大部分,其中的Machine驱动负责Platform和Codec之间的耦合以及部分和设备或板子特定的...

9784
来自专栏专知

2018年SCI期刊最新影响因子排行,最高244,人工智能TPAMI9.455

2018年6月26日,最新的SCI影响因子正式发布,涵盖1万2千篇期刊。CA-Cancer J Clin 依然拔得头筹,其影响因子今年再创新高,达244.585...

1292
来自专栏前端儿

Web 前端颜色值--字体--使用,整理整理

颜色值 CSS 颜色使用组合了红绿蓝颜色值 (RGB) 的十六进制 (hex) 表示法进行定义。对光源进行设置的最低值可以是 0(十六进制 00)。最高值是 2...

2372
来自专栏linux驱动个人学习

高通msm8909耳机调试

1、DTS相应修改: DTS相关代码:kernel/arch/arm/boot/dts/qcom/msm8909-qrd-skuc.dtsi: 1 s...

7625
来自专栏Golang语言社区

Knapsack problem algorithms for my real-life carry-on knapsack

I'm a nomad and live out of one carry-on bag. This means that the total weight o...

1142
来自专栏Ryan Miao

ehcache报错

jfinal2.0+tomcat7+ehcache2.6.11+Linux Linux version 2.6.18-164.el5 (mockbuild@x8...

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

java.base.jmod

/Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home/jmods$ jmod list java....

1112
来自专栏WOLFRAM

向日葵中的数学之美

1863
来自专栏linux驱动个人学习

ALSA声卡驱动的DAPM(一)-DPAM详解

最近使用tinymix 调试相应的音频通道,但是一直不知道音频通道的原理是什么。所以百度了一下,百度结果是与DPAM有关。 一、DAPM简介:  DAPM是Dy...

4796
来自专栏余生开发

echarts太阳分布图-饼图来回穿梭

var dom = document.getElementById("container");

1202

扫码关注云+社区