lambda表达式

lambda表达式简介

lambda 表达式可以用非常少的代码实现抽象方法。lambda 表达式不能被独立执行,因此必须实现函数式接口,并且会返回一个函数式接口的对象。lambda 表达式的语法非常特殊,语法格式如下:

      () -> 结果表达式
     参数 -> 结果表达式
     (参数1, 参数2, … ,参数n) -> 结果表达式
  • 第 1 行实现无参方法,单独写一对圆括号表示方法无参数,操作符右侧的结果表达式表示方法的返回值。

  • 第 2 行实现只有一个参数的方法,参数可以写在圆括号里,或者不写圆括号。

  • 第 3 行实现多参数的方法,所有参数按顺序写在圆括号里,且圆括号不可以省略。

lambda 表达式也可以实现复杂方法,将操作符右侧的结果表达式换成代码块即可,语法格式如下:

      () -> { 代码块 }
     参数 -> { 代码块 }
     (参数1, 参数2, … ,参数n) -> { 代码块 }
  • 第 1 行实现无参方法,方法体是操作符右侧的代码块。

  • 第 2 行实现只有一个参数的方法,方法体是操作符右侧的代码块。

  • 第 3 行实现多参数的方法,方法体是操作符右侧的代码块。

lambda 表达式的语法非常抽象,并且有着非常强大的自动化功能,如自动识别泛型、自动数据类型转换等,这会让初学者很难掌握。如果将 lambda 表达式的功能归纳总结,则可以将 lambda 表达式语法用如下方式理解:

      ()       ->     { 代码块 }
     这个方法   按照  这样的代码来实现

简单总结:操作符左侧的是方法参数,操作符右侧的是方法体。

“→” 符号是由英文状态下的 “-” 和 “>” 组成的,符号之间没有空格。

lambda表达式实现函数式接口

lambda 表达式可以实现函数式接口,本节将讲解函数式接口概念以及用 lambda 表达式实现不同类型的函数式接口。

函数式接口

函数式接口指的是仅包含一个抽象方法的接口,接口中的方法简单明了地说明了接口的用途,如线程接口 Runnable、动作事件监听接口 ActionListener 等。开发者可以创建自定义的函数式接口,例如:

interface MyInterface {
    void method();
}

如果接口中包含一个以上的抽象方法,则不符合函数式接口的规范,这样的接口不能用 lambda 表达式创建匿名对象。本章内容中所有被 lambda 表达式实现的接口均为函数式接口。

lambda表达式实现无参抽象方法

很多函数式接口的抽放方法是无参数的,如线程接口 Runnable 只有一个 run() 方法,这样的无参抽象方法在 lambda 表达式中使用 “( )” 表示。

【例14.1】使用lambda表达式实现打招呼接口(实例位置:资源包\TM\sl\14\1)

创建函数式接口和测试类,接口抽象方法为无参方法并返回一个字符串。使用 lambda 表达式实现接口,让方法可以输出当前日期。

interface SayHiInterface { // 打招呼接口
	String say(); // 打招呼的方法
}

public class NoParamterDemo { // 测试类
	public static void main(String[] args) {
		// lambda表达式实现打招呼接口,返回抽象方法结果
		SayHiInterface pi = () -> "你好啊,这是里lambda表达式";
		System.out.println(pi.say()); 
	}
}

运行结果如下:

     你好啊,这是lambda表达式

本例直接在 lambda 表达式中创建 SayHiInterface 接口对象,并指定了一个字符串作为接口方法的返回值。最后在输出语句中,pi 对象就是 lambda 表达式创建出的对象,当 pi 调用接口方法时就输出了 lambda 表达式指定的字符串。

lambda表达式实现有参抽象方法

抽象方法中有一个或多个参数的函数式接口也是很常见的,lambda 表达式中可以用 “(a1,a2,a3)” 的方法表示有参抽象方法,圆括号里的标识符对应抽象方法的参数。如果抽象方法中只有一个参数,lambda 表达式则可以省略圆括号。

【例14.2】使用lambda表达式做加法计算(实例位置:资源包\TM\sl\14\2)

创建函数式接口和测试类,接口抽象方法有两个参数并返回一个 int 型结果。使用 lambda 表达式实现接口,让方法可以计算两个整数的和,具体代码如下:

interface AdditionInterface {                           // 加法接口
    int add(int a, int b);                              // 加法的抽象方法
}

public class ParamterDemo {                             // 测试类
    public static void main(String[] args) {
        // lambda表达式实现加法接口,返回参数相加的值
        AdditionInterface np = (x, y) -> x + y;
        int result = np.add(15, 26);                   // 调用接口方法
        System.out.println("相加结果:" + result);     // 输出向相加结果
    }
}

运行结果如下:

     相加结果:41

在这个实例中,函数式接口的抽象方法有两个参数,lambda 表达式的圆括号内也写了两个参数对应的抽象方法。这里需要注意以下一点:lambda 表达式中的参数不需要与抽象方法的参数名称相同,但顺序必须相同。

lambda表达式使用代码块

当函数式接口的抽象方法需要实现复杂逻辑而不是返回一个简单的表达式时,就需要在 lambda 表达式中使用代码块。lambda 表达式会自动判断返回值类型是否符合抽象方法的定义。

【例14.3】使用lambda表达式为考试成绩分类(实例位置:资源包\TM\sl\14\3)

创建函数式接口和测试类,接口抽象方法有一个整型参数表示成绩,输入成绩后,返回成绩的字符串评语。在 lambda 表达式中实现成绩判断。

interface CheckGrade {
	String check(int grade); // 查询成绩结果
}

public class GradeDemo {
	public static void main(String[] args) {
		CheckGrade g = (n) -> { // lambda实现代码块
			if (n >= 90 && n <= 100) { // 如果成绩在90-100
				return "成绩为优"; // 输出成绩为优
			} else if (n >= 80 && n < 90) { // 如果成绩在80-89
				return "成绩为良"; // 输出成绩为良
			} else if (n >= 60 && n < 80) { // 如果成绩在60-79
				return "成绩为中"; // 输出成绩为中
			} else if (n >= 0 && n < 60) { // 如果成绩小于60
				return "成绩为差"; // 输出成绩为差
			} else { // 其他数字不是有效成绩
				return "成绩无效"; // 输出成绩无效
			}
		}; // 不要丢掉lambda语句后的分号
		System.out.println(g.check(89)); // 输出查询结果
	}
}

运行结果如下:

     成绩为良

lambda表达式调用外部变量

lambda 表达式除了可以调用定义好的参数,还可以调用表达式以外的变量。但是,这些外部的变量有些可以被更改,有些则不能。例如,lambda 表达式无法更改局部变量的值,但是却可以更改外部类的成员变量(也可以叫作类属性)的值。

lambda表达式无法更改局部变量

局部变量在 lambda 表达式中默认被定义为 final,也就是说,lambda 表达式只能调用局部变量,却不能改变其值。

【例14.4】使用lambda表达式修改局部变量(实例位置:资源包\TM\sl\14\4)

创建函数式接口和测试类,在测试类的 main() 方法中创建局部变量和接口对象,接口对象使用 lambda 表达式予以实现,并在 lambda 表达式中尝试更改局部变量值。

interface VariableInterface1 { 				// 测试接口
    void method(); 						// 测试方法
}

public class VariableDemo1 { 			// 测试类
    public static void main(String[] args) {
        int value = 100;					// 创建局部变量
        VariableInterface1 v = () -> {		// 实现测试接口
            int num = value - 90;			// 使用局部变量赋值
            value = 12; 					// 更改局部变量,此处会报错,无法通过编译
        };
    }
}

在 Eclipse 中编写完这段代码后,会看到更改局部变量的相关代码被标注编译错误,错误提示如图14.1所示,表示局部变量在 lambda 表达式中是以 final 形式存在的。

image 2024 03 05 15 19 41 315
Figure 1. 图14.1 在lambda表达式中更改局部变量会弹出编译错误

lambda表达式可以更改类成员变量

类成员变量在 lambda 表达式中不是被 final 修饰的,因此 lambda 表达式可以改变其值。

【例14.5】使用lambda表达式修改类成员变量(实例位置:资源包\TM\sl\14\5)

创建函数式接口和测试类,在测试类中创建成员属性 value 和成员方法 action()。在 action() 方法中使用 lambda 表达式创建接口对象,并在 lambda 表达式中修改 value 的值。运行程序,查看 value 值是否发生变化。

interface VariableInterface2 {								// 测试接口
    void method(); 										// 测试方法
}

public class VariableDemo2 {								// 测试类
    int value = 100;										// 创建类成员变量
    public void action() { 								// 创建类成员方法
        VariableInterface2 v = () -> {						// 实现测试接口
            value = -12;									// 更改成员变量,没提示任何错误
        };
      
        System.out.println("运行接口方法前value=" + value);		// 运行接口方法前先输出成员变量值
        v.method();										// 运行接口方法
        System.out.println("运行接口方法后value=" + value); 	// 运行接口方法后再输出成员变量值
    }
    public static void main(String[] args) {
        VariableDemo2 demo = new VariableDemo2();			// 创建测试类对象
        demo.action(); 									// 执行测试类方法
    }
}

运行结果如下:

     运行接口方法前value=100
     运行接口方法后value=-12

从这个结果中可以看出以下几点:

  • lambda 表达式可以调用并修改类成员变量的值。

  • lambda 表达式只是描述了抽象方法是如何实现的,在抽象方法没有被调用前,lambda 表达式中的代码并没有被执行,因此在运行抽象方法之前类成员变量的值不会发生变化。

  • 只要抽象方法被调用,就会执行 lambda 表达式中的代码,类成员变量的值也就会被修改。

lambda表达式与异常处理

很多接口的抽象方法为了保证程序的安全性,会在定义时就抛出异常。但是 lambda 表达式中并没有抛出异常的语法,这是因为 lambda 表达式会默认抛出抽象方法原有的异常,当此方法被调用时则需要进行异常处理。

【例14.6】使用lambda表达式实现防沉迷接口(实例位置:资源包\TM\sl\14\6)

创建自定义异常 UnderAgeException,当发现用户是未成年人时进入此异常处理。创建函数式接口,在抽象方法中抛出 UnderAgeException 异常,使用 lambda 表达式实现此接口,并让接口对象执行抽象方法。

import java.util.Scanner;

interface AntiaddictInterface {                         // 防沉迷接口
    boolean check(int age) throws UnderAgeException;    // 抽象检查方法,抛出用户未成年异常
}

class UnderAgeException extends Exception {             // 自定义未成年异常
    public UnderAgeException(String message) {          // 有参构造方法
        super(message);                                 // 调用原有父类构造方法
    }
}

public class ThrowExceptionDemo {                       // 测试类
    public static void main(String[] args) {            // 主方法
        // lambda表达式创建 AntiaddictInterface 对象,默认抛出原有异常
        AntiaddictInterface ai = (a) -> {
            if (a < 18) {
                throw new UnderAgeException("未满18周岁,开启防沉迷模式!");  // 抛出异常
            } else {
                return true;
            }
        };

        Scanner sc = new Scanner(System.in);           // 创建控制台扫描器
        System.out.println("请输入年龄");                // 控制台提示
        int age = sc.nextInt();                        // 获取用户输入的年龄

        try {
            if (ai.check(age)) {
                System.out.println("欢迎进入XX世界");
            } catch(UnderAgeException e) {
                System.err.println(e);
            }
            sc.close();
        }
    }
}

从这个实例中可以看出,即使 lambda 表达式没有定义异常,原抽象方法抛出的异常仍然是存在的,当接口对象执行此方法时会被强制要求进行异常处理。

这段代码中使用了 Scanner 类来获取用户输入的年龄,当用户输入的年龄小于 18 岁时,捕获到 UnderAgeException 异常,运行结果如图14.2所示。

如果用户输入的年龄大于18岁,则不会触发异常处理,直接执行其他业务逻辑,运行效果如图14.3所示。

image 2024 03 05 15 38 31 008
Figure 2. 图14.2 年龄小于18岁会捕获到UnderAgeException异常
image 2024 03 05 15 38 54 278
Figure 3. 图14.3 年龄大于18岁,直接进入XX世界

编程训练(答案位置:资源包\TM\sl\14\编程训练)

【训练1】计算素数 使用lambda表达式创建SingleNumInterface接口对象。抽象方法可以输出方法参数值以内的所有素数。SingleNumInterface接口的定义如下:

     interface SingleNumInterface {
          int[] getSingleNums(int max);
     }

【训练2】小动物吃东西 编写一个Eatable接口,接口中只有一个eat()抽象方法。使用lambda表达式创建3个Eatable接口对象,分别代表小狗、小猫和小鸡,三者执行各自的eat()方法后会输出不同的文本,效果如下:

     dog.eat();    输出  小狗爱吃骨头
     cat.eat();    输出  小猫爱吃鱼
     chick.eat();  输出  小鸡爱吃毛毛虫