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

相关文章

来自专栏风口上的猪的文章

.NET面试题系列[12] - C# 3.0 LINQ的准备工作

"为了使LINQ能够正常工作,代码必须简化到它要求的程度。" - Jon Skeet

923
来自专栏java一日一条

Java 解惑:Comparable 和 Comparator 的区别

Java 中为我们提供了两种比较机制:Comparable 和 Comparator,他们之间有什么区别呢?今天来了解一下。

732
来自专栏程序员互动联盟

【编程基础】Java Comparator接口的使用

在实际编程中我们经常会用到集合或者数组,有的时候你需要对这个集合中的元素就行排序,那这个时候就用到了Comparator接口,先看一下接口的原型: public...

3369
来自专栏程序员的SOD蜜

结构变量作为方法的参数调用,在方法内部使用的“坑”你遇到过吗?

很久没有写博了,今天一个同学在问结构变量的问题,问结构到底是传递值还是传递引用。查过MSDN的都知道,结构默认是传递值的,因此在方法内部,结构的值会被复制一份。...

23810
来自专栏java一日一条

Java Lambda 表达式学习笔记

Java Lambda 表达式是 Java 8 引入的一个新的功能,可以说是模拟函数式编程的一个语法糖,类似于 Javascript 中的闭包,但又有些不同,主...

501
来自专栏菩提树下的杨过

linq to sql的多条件动态查询(下)

借助老外写的一个扩展表达式的类,可以把上篇中的代码写得更优雅 这是PredicateBuilder的源文件 public static class Predic...

2129
来自专栏liulun

30分钟泛型教程

一、泛型入门: 我们先来看一个最为常见的泛型类型List<T>的定义 (真正的定义比这个要复杂的多,我这里删掉了很多东西) [Serializable] pub...

1906
来自专栏闻道于事

Java8Lambda表达式

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

802
来自专栏xiaoxi666的专栏

【模板小程序】 十进制大数相乘(正数、负数、0均可),包含合法性检查

1223
来自专栏浪淘沙

关于数组的算法

3.给定一个数组和一个数num,把小于num的书放在数组左边,大于num的书放在数组右边

1166

扫码关注云+社区