首页 > 编程笔记 > Java笔记 阅读:118

Java Lambda表达式的用法(非常详细)

在数学计算中,Lambda 表达式指的是一个函数,对于输入值的部分或全部组合来说,它会指定一个输出值。

C#JavaScript 里面也都提供了 Lambda 语法,不同语言对于 Lambda 的定义可能不太相同,但相同点是 Lambda 都可当作一个方法,可以输入不同值来返回输出值。

在 Java 中,没有办法编写独立的函数,需要使用方法来代替函数,不过它总是作为对象或类的一部分而存在。现在,Java 语言提供的 Lambda 表达式类似独立函数,可以看成一种匿名方法,拥有更为简洁的语法,可以省略修饰符、返回类型、throws 语句等,在某些情况下还可以省略参数。

Lambda 表达式常用于匿名类并实现方法的地方,以便让 Java 语法更加简洁。

函数式编程思想

函数式编程思想是将计算机运算作为函数的计算,函数的计算可随时调用。

函数式编程语言则是一种编程规范,它将计算机运算视为数学上的函数计算,并且避免使用程序状态以及易变对象;函数除了可以被调用以外,还可以作为参数传递给一个操作,或者作为操作的结果返回。函数式编程语言重点描述的是程序需要完成什么功能,而不是如何一步一步地完成这些功能。

总结来看,函数式编程思想是一种将操作与操作的实施过程进行分离的思想。

诞生 50 多年之后,函数式编程语言(Functional Programming)开始获得越来越多的关注。不仅最古老的函数式语言 Lisp 重获青春,而且新的函数式语言层出不穷,如 Erlang、clojure、Scala、F# 等。

目前,最当红的 Python、JavaScript、Ruby 等语言对函数式编程的支持都很强,就连老牌的面向对象语言 Java、面向过程语言 PHP,都忙不迭地加入对匿名函数的支持。越来越多的迹象表明,函数式编程已经不仅是学术界的最爱,开始大踏步地在业界投入实用。

函数式编程与面向对象编程有很大的区别,它将程序代码当作数学中的函数,函数本身作为另一个函数的参数或返回值,而面向对象编程则是按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,在软件系统就存在什么样的对象。

函数式编程只是对 Java 语言的补充。简而言之,函数式编程尽量忽略面向对象的复杂语法,强调做什么,而不是以什么形式去做;面向对象编程则强调从现实对象的角度来解决问题。

函数式编程思想是 Java 实现并行处理的基础,所以 Java 语言引入了 Lambda 表达式,开启了 Java 语言支持函数式编程的新时代,现在很多语言都支持 Lambda 表达式(不同语言可能叫法不同)。

为什么 Lambda 表达式这么受欢迎呢?这是因为 Lambda 表达式是实现支持函数式编程的技术基础。

Java Lambda表达式语法

Lambda 表达式作用主要是用于匿名内类的方法实现。为了更好地理解 Lambda 表达式的概念,这里先从一个案例开始。

先来使用匿名内部类实现加法运算和减法运算的功能:
interface Calc { // 可计算接口
    int calcInt(int x, int y); // 两个int类型参数
}

public class Demo {
    public static Calc calculate(char opr) {
        Calc result;
        if (opr == '+') {
            result = new Calc() { // 匿名内部类实现Calc接口
                @Override
                public int calcInt(int x, int y) { // 实现加法运算方法
                    return a + b;
                }
            };
        } else {
            result = new Calc() { // 匿名内部类实现Calc接口
                @Override
                public int calcInt(int x, int y) { // 实现减法运算方法
                    return a - b;
                }
            };
        }
        return result;
    }

    public static void main(String[] args) {
        int n1 = 10;
        int n2 = 5;
        Calc f1 = calculate('+'); // 实现加法计算
        Calc f2 = calculate('-'); // 实现减法计算
        System.out.println(n1 + "+" + n2 + "=" + f1.calcInt(n1, n2));
        System.out.println(n1 + "-" + n2 + "=" + f2.calcInt(n1, n2));
    }
}
程序的运行结果如下:

10+5=15
10-5=5

程序中,calculate() 方法的参数是具体的操作数,返回值类型是 Calc 接口,代码第 11 行和第 18 行都采用匿名内部类实现了 Calc 接口的 calcInt() 方法,第 12 行实现加法运算,第 19 行实现减法运算。

但是,上述使用匿名内部类实现通用方法 calculate() 的代码很臃肿。现在,我们采用 Lambda 表达式来替代匿名内部类,修改 Demo 类,修改之后 calculate() 的代码如下:
/**
* 通过操作符进行计算
* @param opr 操作符
* @return 实现Calc接口对象
*/
public static Calc calculate(char opr) {
    Calc result;
    if (opr == '+') {
        result = (int a, int b) -> { // Lambda表达式实现Calc接口
            return a + b;
        };
    } else {
        result = (int a, int b) -> { // Lambda表达式实现Calc接口
            return a - b;
        };
    }
    return result;
}
代码第 9 行和第 13 行用 Lambda 表达式替代匿名内部类,程序运行结果和实例 1 相同。因为 Lambda 表达式和匿名类都是为了作为传递给方法的参数而设立的,它们都可以把功能像对象一样传递给方法。使用匿名类是向方法传递了一个对象,而使用 Lambda 表达式不需要创建对象,只需要将 Lambda 表达式传递给方法即可。

通过上述演示,可以给 Lambda 表达式一个定义:Lambda 表达式是一个匿名函数(方法)代码块,可以作为表达式、方法参数和方法返回值。

完整的 Lambda 表达式有 3 个要素,分别是参数列表、箭头符号、代码块,语法格式如下:
(参数列表) -> {
    …    // Lambda表达式体
}
这里,针对 Lambda 表达式的 3 个要素说明如下:
下面是几个 Lambda 表达式的简单示例:
(int a,int b) -> a + b;                     // 两个参数a和b,返回二者的和
() -> 79;                                   // 没有参数,返回整数79
(String str) -> {System.out.println(str);}  // String类型参数,打印到控制台
(int a) -> {return a + 1;}                  // 以一个整数为参数,返回该数加1后的值
观察上述几个简单示例,除了刚才必备的 3 个要素之外,发现 Lambda 表达式像没有名字的方法,并且它没有返回类型、throws 子句。

实际上,返回类型和异常是由 Java 编译器自动从 Lambda 表达式的代码块得到的,如上述示例中最后一个 Lambda 表达式,由于 a 为 int 类型,故而返回类型是 int,而 throws 子句为空。因此,Lambda 表达式真正缺少的是方法名称,从这个角度来讲,Lambda 表达式可以视为一种匿名方法,这点和匿名类相似。

使用匿名类需要向方法传递一个对象,而使用 Lambda 表达式则不需要创建对象,只需要将表达式传递给方法。所以,Lambda 表达式语法上比匿名类更加简单、代码更少、逻辑上更清晰。

Java函数式接口

由于 Lambda 表达式的返回值类型由代码块决定,所以 Lambda 表达式可以作为“任意类型”的对象传递给调用者,具体作为何种类型的对象,取决于调用者的需要。

为了能够确定 Lambda 表达式的类型,而又不对 Java 的类型系统做大的修改,Java 利用现有的 interface 接口来作为 Lambda 表达式的目标类型,这种接口被称为函数式接口。

函数式接口本质上就是只包含一个抽象方法的接口,也可以包含多个默认方法、类方法,但只能声明一个抽象方法,如果声明多个抽象方法,则会发生编译错误。

查看 Java 8 之后的 API 文档,可以发现大量的函数式接口,如 Runnable、ActionListener 等。

JDK 8 之后,为函数式接口提供了一个新注解 @FunctionalInterface,放在定义的接口前面,用于告知编译器执行更严格的检查,防止在函数式接口中声明多个抽象方法,即检查该接口必须是函数式接口,否则编译器报错。

由于 Lambda 表达式的结果被作为对象,在程序中完全可以使用 Lambda 表达式进行赋值,参考如下代码:
@FunctionalInterface
public interface Runnable {      // Runnable是Java提供的一个接口
    public abstract void run();  // Runnable在接口中只包含一个无参数的方法
}
Runnable runnable = () -> {
    for(var i = 0;i < 99;i++) {
        System.out.println(i);
    }
};
通过上述代码可以发现,Lambda 表达式代表的匿名方法实现了 Runnable 接口中唯一的、无参数的方法。

为了保证 Lambda 表达式的目标类型是一个明确的函数式接口,有如下 3 种常见方式:
Lambda 表达式可以自行定义函数式接口,如果是常用的功能,则有点太麻烦。为了方便开发者使用,Java已经定义了几种通用的函数式接口,用户可以基于这些通用接口来编写程序。

JDK 8 新增的函数式接口都放在 java.util.function 包下,最常用的有 4 类,如下表所示。

表:Java 提供的函数式接口
函数式接口 方法 方法描述
Consumer<T> void accept(T t) 消费性接口,提供的是无返回值的抽象方法
Supplier <T> T get() 提供的是有返无参的抽象方法
Function<T,R> R apply(T t) 提供的是有参有返的抽象方法
Predicate <T> boolean test(T t) 提供的有参有返的方法,返回的是 boolean 类型的返回值

这里,以 Predicate<T> 接口为例展开对函数式接口的讨论。该接口接收一个布尔型的表达式参数,而数据的集合类 ArrayList 有一个 removeIf() 方法,它的参数是 Predicate 类型,是专门用来传递 Lambda 表达式的。

接下来,通过案例来演示函数式接口传递 Lambda 表达式的功能。
import java.util.ArrayList;
import java.util.List;

public class Demo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(); // 定义一个List集合
        list.add("Java"); // 向list集合追加数据
        list.add("Es6");
        list.add(null);
        list.add("Html5");
        list.add(null);

        System.out.println(list); // 输出集合中的数据
        list.removeIf((e) -> { // 使用Predicate<T>进行去null处理
            return e == null;
        });
        System.out.println(list); // 输出删除之后的数据
    }
}
程序的运行结果如下:

[Java, Es6, null, Html5, null]
[Java, Es6, Html5]

程序中,先创建一个名字为 list 的 ArrayList 集合,然后向 list 中添加了 5 个元素,接着输出 list 集合中元素。然后又通过调用 list 的 removeIf() 方法,传递符合 Predicate 函数式接口的 Lambda 表达式。因为传递的元素是判断参数等于 null,所以会删除 list 中值为 null 的元素,接下来输出 list 中的数据,结果发现值为 null 的元素被删除了。

综上所述,函数式接口带给我们最大的好处就是,可以使用极简的 lambda 表达式实例化接口,这点在实际的开发中很有好处,往往一两行代码能够解决很复杂的场景需求。

注意,@FunctionalInterface 注解加或不加对于接口是不是函数式接口没有影响,该注解只是提醒编译器去检查该接口是否仅包含一个抽象方法。

Java Lambda表达式的简化形式

Lambda 表达式的核心原则是:只要可以推导,都可以省略,即可以根据上下文推导出来的内容,都可以省略书写,这样简化了代码,但潜在的问题是有可能会使代码可读性变差。

1) 省略大括号

在 Lambda 表达式中,如果程序代码块只包含了一条语句,就可以省略大括号。标准格式和省略格式对比如下:
() -> {System.out.prnitln("一起来跟我学习JAVA的Lambda表达式");}  // 标准格式
() -> System.out.prnitln("一起来跟我学习JAVA的Lambda表达式");  // 省略格式

2) 省略参数类型

Lambda 表达式可以根据上下文环境推断出参数类型,所以可以省略参数类型。标准格式和省略格式对比如下:
interface Calc { // 可计算接口
    int calcInt(int x, int y); // 两个int类型参数
}

public static Calc calculate(char opr) {
    Calc result;
    if (opr == '+') {
        result = (int x, int y) -> { // 标准格式
            return x + y;
        };
    } else {
        result = (x, y) -> { // 省略格式
            return x - y;
        };
    }
    return result;
}

3) 省略圆括号

当 Lambda 表达式中参数只有一个的时候,可以省略参数圆括号。

下面代码使用到了前面的 Consumer 函数式接口,Lambda 表达式标准格式和省略圆括号的格式对比如下:
Consumer<String>consumer = (s) -> System.out.println(s);  // 标准格式
Consumer<String>consumer = s -> System.out.println(s);  // 省略格式
consumer.accept("一起来学习Java");            // 简化形式的调用

4) 省略return和大括号

当 Lambda 表达式的代码块中有返回值且有只有一条语句时,那么可以省略 return 和大括号。注意,二者需要同时省略,否则编译报错。

下面代码使用到了 Comparator 接口,该接口包含 compare(T o1,T o2) 方法,此方法有两个泛型参数可以进行比较,会返回 int 类型值。返回值大于 0,表示第 1 个参数大;返回值等于 0,表示两个参数相同;返回值小于 0,表示第 2 个参数较大。

Lambda 表达式标准格式和省略格式对比如下:
Comparator<Integer> com = (x, y) -> {return Integer.compare(x,y);}  // 标准格式
Comparator<Integer> com = (x, y) -> Integer.compare(x,y);       // 省略格式
System.out.println(com.compare(3,3));                 // 简化形式的调用
上述 4 种方式是 Lambda 表达式的简化形式,代码简洁了,对于初学者而言会增加理解难度,一般建议初学者使用标准格式,等熟练掌握 Lambda 表达式后再逐步使用简化形式。

Java Lambda访问变量

Lambda 表达式中可以访问其外层作用域中定义的变量。

例如,可以使用其外层类定义的实例或静态变量以及调用其外层类定义的方法,也可以显式或隐式地访问 this 变量。

1) 访问成员变量

成员变量包括实例成员变量和静态成员变量。在 Lambda 表达式中,可以访问这些成员变量,此时的 Lambda 表达式与普通方法一样,可以读取成员变量,也可以修改成员变量。

接下来,通过案例来演示 Lambda 表达式访问成员变量的功能:
interface Calc {
    int calcInt(int x, int y);
}

public class Demo {
    private int count = 1; // 实例成员变量
    private static int num = 2; // 静态成员变量

    public static Calc add() { // 静态方法,进行加法运算
        Calc result = (int x, int y) -> {
            num++; // 访问静态成员变量,不能访问实例成员变量
            int c = x + y + num; // 修改为x + y + num+this.count会报错
            return c;
        };
        return result;
    }

    public Calc mul() { // 实例方法,进行乘法运算
        Calc result = (int x, int y) -> {
            num++;
            this.count++; // 访问静态成员变量和实例成员变量
            int c = x * y - num - this.count;
            return c;
        };
        return result;
    }

    // 测试方法
    public static void main(String[] args) {
        System.out.println("静态方法,加法运算:" + add().calcInt(4, 3));
        System.out.println("实例方法,减法运算:" + new Demo().mul().calcInt(4, 3));
    }
}
程序的运行结果如下:

静态方法,加法运算:10
实例方法,减法运算:6

从程序运行结果来看,程序中声明了一个实例成员变量 count 和一个静态成员变量 num。此外,还声明了静态方法 add() 和实例方法 sub()。

add()方法是静态方法,静态方法中不能访问实例成员变量,所以第 10 行代码的 Lambda 表达式中也不能访问实例成员变量,在代码“x +y + num”后加上 this.count 会报错。

sub() 方法是实例方法,实例方法中能够访问静态成员变量和实例成员变量,所以第 19 行代码的 Lambda表达式中可以访问这些变量。

当然,实例方法和静态方法也可以访问,当访问实例成员变量或实例方法时可以使用 this,在不与局部变量发生冲突情况下可以省略 this。

Java Lambda捕获局部变量

对于成员变量的访问,Lambda 表达式与普通方法没有区别,但是有时候 Lambda 表达式需要访问外部作用域代码中的变量,这些变量不是在函数体内定义的,是在 Lambda 表达式所处的上下文中定义的,这称为变量捕获或变量绑定。

当 Lambda 表达式发生变量捕获时,系统编译器会将变量当成 final 的。因此,这些变量在声明时,可以不定义成 final,并且 Lambda 表达式中不能修改那些捕获的变量。

接下来,通过案例来演示如何捕获局部变量:
import java.util.Comparator;

public class Demo {
    public static void main(String[] args) {
        test();
    }

    static void test() {
        Integer a = 222;
        Comparator<Integer> com = (x, y) -> {
            // 如果取消注释会报错,下面会详细解释
            // a++;
            return Integer.compare(x, y);
        };
        System.out.println("两个数字比较结果为:" + com.compare(22, 33));
    }
}
程序的运行结果如下:

两个数字比较结果为:-1

程序中,使用 Comparator<Integer> 比较接口,第 10~14 行代码为 Lambda 表达式,可以访问其外部域第 9 行代码,系统自动将第 9 行代码当成 final 类型的变量,此处显式加上 final 也可以。

如果取消第 12 行代码注释,则程序编译报错,提示“Variable used in lambda expression should be final or effectively final”,中文含义为“从Lambda表达式引用的本地变量必须是最终变量或实际上的最终变量”。出现错误的原因在于,代码中声明了局部变量 a,Lambda 表达式中捕获这个变量,不管这个变量是否显式地使用 final 修饰,它都不能在 Lambda 表达式中被修改。

Lambda 表达式的代码块可以访问外部作用域的变量,意味着 Lambda 表达式的方法体与外部作用域的代码块有相同的作用域范围,所以在 Lambda 表达式范围内不允许声明一个与局部变量名相同的参数或局部变量。

如果把第 10 行代码中的变量 x 修改为 a,程序编译报错,提示“Variable 'a' is already defined in the scope”,中文含义为“变量 a已经在局部范围内定义”,出现错误的原因就是,在方法 test() 内部不能有两个同名的变量。

Java Lambda表达式调用Arrays的类方法

Arrays 类的一些方法需要实现 Comparator、XxxOperator、XxxFunction 等接口的实例,这些接口都是函数式接口,因此可以使用 Lambda 表达式来调用 Arrays 的方法。

接下来,通过案例来演示使用 Lambda 表达式调用 Arrays 的类方法。
import java.util.Arrays;

public class Demo {
    public static void main(String[] args) {
        String[] arr = new String[] { "CSDN", "51CTO", "ITEye", "cnblogs" };
        Arrays.parallelSort(arr, (s1, s2) -> s1.length() - s2.length());
        System.out.println("排序:" + Arrays.toString(arr));

        int[] intArray = new int[] {3, 9, 8, 0};
        // left代表数组中前一个索引处的元素,计算第1个元素时left为1
        // right代表数组中当前索引处的元素
        Arrays.parallelPrefix(intArray, (left, right) -> left * right);
        System.out.println("累积:" + Arrays.toString(intArray));

        long[] longArray = new long[5];
        // operand代表正在计算的元素索引
        Arrays.parallelSetAll(longArray, operand -> operand * 5);
        System.out.println("索引*5:" + Arrays.toString(longArray));
    }
}
程序的运行结果如下:
排序:[CSDN, 51CTO, ITEye, CNBLOGS]
累积:[3, 27, 216, 0]
索引*5:[0, 5, 10, 15, 20]
通过程序运行结果可以发现:
通过本案例可以发现,Lambda 表达式能够使程序更加简洁,代码更加简单。

相关文章