摄于 重庆-南山一棵树
阅读项目代码时,尤其是阅读一些源码时,经常会遇到 Lambda 表达式。对此之前看过相关文章,但是停留在模模糊糊的印象上。今天趁着有时间,通过一些 demo 示例,梳理一下它的用法,以备后期遗忘的时候快速查询它的用法!
Lambda 表达式是 Java 8 的重要更新,它支持将代码块作为方法参数、允许使用更简洁的代码来创建只有一个抽象方法的接口的实例。
描述中提到的接口称为函数式接口
Lambda 表达式的主要作用就是可以用于简化创建匿名内部类对象,Lambda 表达式的代码块将会用于实现抽象方法的方法体,Lambda 表达式就相当于一个匿名方法。
Lambda 表达式由三部分组成:
形参列表:形参列表允许省略类型,如果形参列表中只有一个参数,形参列表的圆括号也可以省略;
箭头( ->
):通过英文画线和大于符号组成;
代码块:如果代码块只有一条语句,花括号可以省略。Lambda 代码块只有一条 return 语句,可以省略 return 关键字,Lambda 表达式会自动返回这条语句的值作为返回值。
1interface Eatable { 2 void taste(); 3} 4 5interface Flyable { 6 void fly(String weather); 7} 8 9interface Addable { 10 int add(int a, int b); 11} 12 13 14public class LambdaQs { 15 // 调用该方法需要传入一个 Eatable 类型的对象 16 public void eat(Eatable e) { 17 System.out.println(e); 18 e.taste(); 19 } 20 21 // 调用该方法需要传入 Flyable 类型的对象 22 public void drive(Flyable f) { 23 System.out.println("我正在驾驶:" + f); 24 f.fly("「夏日晴天」"); 25 } 26 27 // 调用该方法需要 Addable 类型的对象 28 public void calc(Addable add) { 29 System.out.println("5 + 3 = " + add.add(5, 3)); 30 } 31 32 public static void main(String[] args) { 33 LambdaQs lq = new LambdaQs(); 34 // Lambda 表达式的代码块只有一句,因此省略了花括号 35 lq.eat(() -> System.out.println("雪糕的味道不错!")); 36 // Lambda 表达式的形参只有一个参数,因此省略了圆括号 37 lq.drive(weather -> { 38 // 对接口中抽象方法 fly 的重写 39 System.out.println("今天天气是:" + weather); 40 System.out.println("飞机平稳飞行!"); 41 }); 42 // Lambda 表达式只有一条语句,即使该表达式需要返回值,也可以省略 return 43 lq.calc((a, b) -> a + b); 44 // 如果不用 Lambda 表达式,就需要如下匿名类的方式去重写抽象方法 45 lq.calc(new Addable() { 46 @Override 47 public int add(int a, int b) { 48 return a + b; 49 } 50 }); 51 } 52}
输出结果:
1oop.lambda.LambdaQs$$Lambda$1/1607521710@7ef20235 2雪糕的味道不错! 3我正在驾驶:oop.lambda.LambdaQs$$Lambda$2/1329552164@15aeb7ab 4今天天气是:「夏日晴天」 5飞机平稳飞行! 65 + 3 = 8 75 + 3 = 8
以上示例可以说明,Lambda 表达式实际上可以被当做一个具体的对象。
Lambda 表达式与函数式接口
Lambda 表达式的类型,也被称为「目标类型( target type
)」。 Lambda 表达式的目标类型必须是「函数式接口( functional interface
)」 。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但仅能声明一个抽象方法。
查询 Java 8 的 API 文档,可以发现大量的函数式接口,例如:Runnable、ActionListener 等接口都是函数式接口。
Java 8 专门为函数式接口提供了 @FunctionalInterface
注解。该注解就是用于告诉编译器校验接口必须是函数式接口,否则就报错。
由于 Lambda 表达式的结果就是被当做对象/实例,因此,可以使用 Lambda 表达式进行赋值,示例:
1Runnable r = () -> { 2 for (int i = 0; i < 100; i++) { 3 System.out.println(i); 4 } 5};
我们看一下 Runnable 接口的定义:
1@FunctionalInterface 2public interface Runnable { 3 public abstract void run(); 4}
看一个错误示例:
1Object obj = () -> { 2 for (int i = 0; i < 100; i++) { 3 System.out.println(i); 4 } 5};
上面这段代码会报错: Target type of a lambda conversion must be an interface
。Lambda 表达式的目标类型必须是明确的函数式接口!将 Lambda 表达式赋值给 Object 类型的变量,编译器只能推断出它的表达类型为 Object,而 Object 并不是函数式接口,因此就报错了!
为了保证 Lambda 表达式的目标类型是明确的函数式接口,有如下三种常见方式:
将 Lambda 表达式赋值给函数式接口类型的变量;
将 Lambda 表达式作为函数式接口类型的参数传给某个方法;
使用函数式接口对 Lambda 表达式进行强制类型转换;
将上面出错的代码可以进行如下的改写:
1Object obj1 = (Runnable)() -> { 2 for (int i = 0; i < 100; i++) { 3 System.out.println(i); 4 } 5};
综上,Lambda 表达式的本质很简单,就是使用简单的语法来创建函数式接口的实例,避免匿名内部类的繁琐。
如果 Lambda 表达式的代码块只有一条代码,还可以在代码中使用方法引用和构造器引用。
方法引用和构造器引用的好处是使 Lambda 表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号 ::
。
1@FunctionalInterface 2interface Converter { 3 Integer convert(String from); 4} 5 6@FunctionalInterface 7interface MyTest { 8 String test(String a, int b, int c); 9} 10 11@FunctionalInterface 12interface YourTest { 13 // 抽象方法负责根据 String 参数生成一个 JFrame 返回值 14 JFrame win(String title); 15} 16 17 18public class LambdaRef { 19 public static void main(String[] args) { 20 // 1 引用类方法 21 // 下面使用 Lambda 表达式创建 Converter 对象 22 Converter converter1 = from -> Integer.valueOf(from); 23 Integer val = converter1.convert("99"); 24 25 // 函数式接口中被实现方法的全部参数传给该类方法作为参数 26 Converter converter2 = Integer::valueOf; 27 Integer val2 = converter2.convert("100"); 28 29 // 2 引用特定对象的实例方法 30 // 使用 Lmabda 表达式创建 Converter 对象 31 Converter converter3 = from -> "hello michael翔".indexOf(from); 32 33 // 调用 "hello michael翔"的indexOf()实例方法 34 // 函数式接口中被实现的全部参数传给该方法作为参数 35 Converter converter4 = "hello michael翔"::indexOf; 36 37 // 3 引用某类对象的实例方法 38 // 使用 Lambda 表达式创建 MyTest 对象 39 MyTest mt = (a, b, c) -> a.substring(b, c); 40 String str = mt.test("Hello World, Hello Michael翔", 2,9); 41 42 // 上面 Lambda 表达式只有一行,因此可以使用如下引用进行替换 43 // 函数式接口中被实现方法的第一个参数作为调用者 44 // 后面的参数全部传给该方法作为参数 45 MyTest str2 = String::substring; 46 47 // 4 引用构造器 48 // 使用 Lambda 表达式创建 YourTest 对象 49 YourTest yt = a -> new JFrame(a); 50 JFrame jf = yt.win("窗口"); 51 52 // 使用构造器引用进行替换 53 // 函数式接口中被实现方法的全部参数传给该构造器作为参数 54 YourTest yt2 = JFrame::new; 55 JFrame jf2 = yt.win("窗口2"); 56 } 57}
Lambda 表达式与匿名内部类存在如下相同点:
Lambda 表达式与匿名内部类一样,都可以直接访问 effectively final
的局部变量,以及外部类的成员变量(包括示例变量和类变量);
Lambda 表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法;
Lambda 表达式与匿名内部类的区别:
匿名内部类可以为任意接口创建实例,不管接口包含多少个抽象方法,只要匿名内部类实现所有抽象方法即可;但是 Lambda 表达式只能为函数式接口创建实例;
匿名内部类可以为抽象类甚至普通类创建实例,但是 Lambda 表达式只能为函数式接口创建实例;
匿名内部类实现的抽象方法体允许调用接口中定义的默认方法,但是 Lambda 表达式的代码块不允许调用接口中定义的默认方法;
1@FunctionalInterface 2interface Converter { 3 Integer convert(String from); 4} 5 6@FunctionalInterface 7interface MyTest { 8 String test(String a, int b, int c); 9} 10 11@FunctionalInterface 12interface YourTest { 13 // 抽象方法负责根据 String 参数生成一个 JFrame 返回值 14 JFrame win(String title); 15} 16 17 18public class LambdaRef { 19 public static void main(String[] args) { 20 // 1 引用类方法 21 // 下面使用 Lambda 表达式创建 Converter 对象 22 Converter converter1 = from -> Integer.valueOf(from); 23 Integer val = converter1.convert("99"); 24 25 // 函数式接口中被实现方法的全部参数传给该类方法作为参数 26 Converter converter2 = Integer::valueOf; 27 Integer val2 = converter2.convert("100"); 28 29 // 2 引用特定对象的实例方法 30 // 使用 Lmabda 表达式创建 Converter 对象 31 Converter converter3 = from -> "hello michael翔".indexOf(from); 32 33 // 调用 "hello michael翔"的indexOf()实例方法 34 // 函数式接口中被实现的全部参数传给该方法作为参数 35 Converter converter4 = "hello michael翔"::indexOf; 36 37 // 3 引用某类对象的实例方法 38 // 使用 Lambda 表达式创建 MyTest 对象 39 MyTest mt = (a, b, c) -> a.substring(b, c); 40 String str = mt.test("Hello World, Hello Michael翔", 2,9); 41 42 // 上面 Lambda 表达式只有一行,因此可以使用如下引用进行替换 43 // 函数式接口中被实现方法的第一个参数作为调用者 44 // 后面的参数全部传给该方法作为参数 45 MyTest str2 = String::substring; 46 47 // 4 引用构造器 48 // 使用 Lambda 表达式创建 YourTest 对象 49 YourTest yt = a -> new JFrame(a); 50 JFrame jf = yt.win("窗口"); 51 52 // 使用构造器引用进行替换 53 // 函数式接口中被实现方法的全部参数传给该构造器作为参数 54 YourTest yt2 = JFrame::new; 55 JFrame jf2 = yt.win("窗口2"); 56 } 57}
Arrays 类的有些方法需要 Comparator、XxxOperator、XxxFunction 等接口的实例,这些接口都是函数式接口。因此,可以使用 Lambda 表达式来调用 Arrays 的方法。
1public class LambdaArrays { 2 public static void main(String[] args) { 3 String[] arr1 = new String[]{"java", "python", "rust", "go"}; 4 Arrays.parallelSort(arr1, (o1, o2) -> o1.length() - o2.length()); 5 System.out.println(Arrays.toString(arr1)); 6 int[] arr2 = {3, -4, 25, 16, 30, 18}; 7 // left 代表数组中前一个索引处的元素,计算第一个元素时,left 为 1; 8 // right 代表数组中的当前索引处的元素 9 Arrays.parallelPrefix(arr2, (left, right) -> left * right); 10 System.out.println(Arrays.toString(arr2)); 11 long[] arr3 = new long[5]; 12 // a 代表正在计算的元素索引 13 Arrays.parallelSetAll(arr3, a -> a * 5); 14 System.out.println(Arrays.toString(arr3)); 15 16 // 等价于用匿名内部类重写 applyAsLong 抽象方法 17 Arrays.parallelSetAll(arr3, new IntToLongFunction() { 18 @Override 19 public long applyAsLong(int value) { 20 return value * 5; 21 } 22 }); 23 System.out.println(Arrays.toString(arr3)); 24 } 25}
输出:
1[go, java, rust, python] 2[3, -12, -300, -4800, -144000, -2592000] 3[0, 5, 10, 15, 20] 4[0, 5, 10, 15, 20]
因为这些要输入 Comparator、XxxOperator、XxxFunction 等接口的实例往往都是一次性的,使用 Lambda 表达式也不用考虑重用等,反而让程序更加简洁了。
本文主要参考的是 《疯狂 Java 讲义第 5 版》的第 6 章的面向对象下,通过实际的示例 demo 应该可以将 Lambda 的常用场景和用法掌握了。这样,看项目代码或者源码的话,会更加易于理解!基本功扎实,才能走得更快!
To Be Top Javaer/糖块十二、Lambda表达式
生命不息,折腾不止!关注 「Coder 魔法院」,祝你 Niubilitiy !:ox::beer:
往期文章
Git 基本命令 merge 和 rebase,你真的了解吗?
Chrome 浏览器收藏夹里的那些宝藏全给你!
IDEA 插件推荐 —— 让你写出好代码的神器!
好看的人才能点