JAVA编程讲义之JDK17.JDK8 Lambda表达式
Lambda表达式(Lambda Expression)是JDK8新增的功能,它显著增强了Java,继续保持自身的活力和创造性。它基于数学中的演算得名,是一个匿名函数,即没有函数名的函数,主要优点在于简化代码、增强代码可读性、并行操作集合等。Lambda表达式正在重塑Java,将影响到后续Java技术的使用。方法引用可以理解为Lambda表达式的快捷写法,它比Lambda表达式更加的简洁,可读性更高,有更好的重用性。本章将对Lambda表达式和方法引用展开详细讲解。
10.1 Lambda表达式入门
在数学计算中,Lambda表达式指的是一个函数:对于输入值的部分或全部组合来说,它会指定一个输出值。在C#、JavaScript里面也都提供了Lambda语法,不同语言对于Lambda的定义可能不太相同,但相同点是Lambda都可当作一个方法,可以输入不同值来返回输出值。在Java中,没有办法编写独立的函数,需要使用方法来代替函数,不过它总是作为对象或类的一部分而存在。现在,Java语言提供的Lambda表达式类似独立函数,可以看成一种匿名方法,拥有更为简洁的语法,可以省略修饰符、返回类型、throws语句等,在某些情况下还可以省略参数。Lambda表达式常用于匿名类并实现方法的地方,以便让Java语法更加简洁。
10.1.1 函数式编程思想
函数式编程思想是将计算机运算作为函数的计算,函数的计算可随时调用。函数式编程语言则是一种编程规范,它将计算机运算视为数学上的函数计算,并且避免使用程序状态以及易变对象;函数除了可以被调用以外,还可以作为参数传递给一个操作,或者作为操作的结果返回。函数式编程语言重点描述的是程序需要完成什么功能,而不是如何一步一步地完成这些功能。总结来看,函数式编程思想是一种将操作与操作的实施过程进行分离的思想。
诞生50多年之后,函数式编程语言(functional programming)开始获得越来越多的关注。不仅最古老的函数式语言Lisp重获青春,而且新的函数式语言层出不穷,如Erlang、clojure、Scala、F#等。目前,最当红的Python、Javascript、Ruby等语言对函数式编程的支持都很强,就连老牌的面向对象语言Java、面向过程语言PHP,都忙不迭地加入对匿名函数的支持。越来越多的迹象表明,函数式编程已经不仅是学术界的最爱,开始大踏步地在业界投入实用。
函数式编程与面向对象编程有很大的区别,它将程序代码当作数学中的函数,函数本身作为另一个函数的参数或返回值,而面向对象编程则是按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,在软件系统就存在什么样的实体。函数式编程只是对Java语言的补充。简而言之,函数式编程尽量忽略面向对象的复杂语法,强调做什么,而不是以什么形式去做;面向对象编程则强调以现实对象的角度来解决问题。
函数式编程思想是Java实现并行处理的基础,所以Java语言引入了Lambda表达式,开启了Java语言支持函数式编程 (Functional Programming)新时代,现在很多语言都支持Lambda表达式(不同语言可能叫法不同),如C#、Swift和JavaScript等。为什么Lambda表达式这这么受欢迎呢?这是因为Lambda表达式是实现支持函数式编程的技术基础。
10.1.2 Lambda表达式语法
第7.3.4节已经讲过匿名内部类,Lambda表达式作用则主要是用于匿名内类的方法实现。为了更好的理解Lambda表达式的概念,这里先从一个案例开始。如例10-1所示,先来使用匿名内部类实现加法运算和减法运算的功能。
例10-1 Demo1001.java
1 package com.aaa.p100102;
1
1 interface Calc { // 可计算接口
1 int calcInt(int x, int y); // 两个int类型参数
1 }
1 public class Demo1001 {
1 public static Calc calculate(char opr) {
1 Calc result;
1 if (opr == '+') {
1 result = new Calc() { // 匿名内部类实现Calc接口
1 @Override
1 public int calcInt(int x, int y) { // 实现加法运算方法
1 return a + b;
1 }
1 };
1 } else {
1 result = new Calc() { // 匿名内部类实现Calc接口
1 @Override
1 public int calcInt(int x, int y) { // 实现减法运算方法
1 return a - b;
1 }
1 };
1 }
1 return result;
1 }
1 public static void main(String[] args) {
1 int n1 = 10;
1 int n2 = 5;
1 Calc f1 = calculate('+'); // 实现加法计算
1 Calc f2 = calculate('-'); // 实现减法计算
1 System.out.println(n1 + "+" + n2 + "=" + f1.calcInt(n1, n2));
1 System.out.println(n1 + "-" + n2 + "=" + f2.calcInt(n1, n2));
1 }
1 }
程序的运行结果如下:
10+5=15
10-5=5
例10-1中,calculate()方法的参数是具体的操作数,返回值类型是Calc接口,代码第12行和第19行都采用匿名内部类实现了Calc接口的calcInt()方法,第13行实现加法运算,第20行实现减法运算。
但是,上述使用匿名内部类实现通用方法calculate()的代码很臃肿。现在,我们采用 Lambda 表达式来替代匿名内部类,修改Demo1001类,修改之后calculate()的代码如下:
1 /**
2 * 通过操作符,进行计算
3 * @param opr 操作符
4 * @return 实现Calc接口对象
5 */
6 public static Calc calculate(char opr) {
7 Calc result;
8 if (opr == '+') {
9 result = (int a, int b) -> { // Lambda表达式实现Calc接口
10 return a + b;
11 };
12 } else {
13 result = (int a, int b) -> { // Lambda表达式实现Calc接口
14 return a - b;
15 };
16 }
17 return result;
18 }
代码第9行和第13行用 Lambda 表达式替代匿名内部类,程序运行结果和案例10-1相同。因为Lambda表达式和匿名类都是为了作为传递给方法的参数而设立的,它们都可以把功能像对象一样传递给方法。使用匿名类是向方法传递了一个对象,而使用Lambda表达式不需要创建对象,只需要将Lambda表达式传递给方法即可。
通过上述演示,可以给Lambda表达式一个定义:Lambda表达式是一个匿名函数(方法)代码块,可以作为表达式、方法参数和方法返回值。
完整的Lambda表达式有3个要素,分别是参数列表、箭头符号、代码块,语法格式如下:
(参数列表) -> {
… // Lambda表达式体
}
这里,针对Lambda表达式的3个要素说明如下:
参数列表:当只有一个参数时无需定义圆括号,但无参数或多个参数需要定义圆括号。
箭头符号:箭头符号“->”指向后面要做的事情。
代码块:如果主体包含了一个语句,就不需要使用大括号{};如果是多行语句,则该语句块会像方法体一样被执行,并由一个return语句返回到调用者。
下面是几个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表达式语法上比匿名类更加简单、代码更少、逻辑上更清晰。
10.2 函数式接口
由于Lambda表达式的返回值类型由代码块决定,所以Lambda表达式可以作为“任意类型”的对象传递给调用者,具体作为何种类型的对象,取决于调用者的需要。为了能够确定Lambda表达式的类型,而又不对Java的类型系统做大的修改,Java利用现有的interface接口来作为Lamba表达式的目标类型,这种接口被称为函数式接口。函数式接口本质上就是只包含一个抽象方法的接口,也可以包含多个默认方法、类方法、但只能声明一个抽象方法,如果声明多个抽象方法,则会发生编译错误。
查看Java 8之后的API文档,可以发现大量的函数式接口,如Runnable、ActionListener等。JDK8之后,为函数式接口提供了一个新注解@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表达式赋值给函数式接口类型的变量。
将Lambda表达式作为函数式接口类型的参数传给某个方法。
使用函数式接口对Lambda表达式进行强制类型转换。
Lambda表达式可以自行定义函数接口,如果是常用的功能,则有点太麻烦。为了方便开发者使用,Java已经定义了几种通用的函数接口,用户可以基于这些通用接口来编写程序。JDK1.8新增的函数式接口都放在java.util.function包下,最常用的有4类,如表10.1所示。
表10.1 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表达式的(集合类ArrayList在后续第11.3.2节详细讲解)。
接下来,通过案例来演示函数式接口传递Lambda表达式的功能,如例10-2所示。
例10-2 Demo1002.java
1 import java.util.ArrayList;
2 import java.util.List;
3
4 public class Demo1002 {
5 public static void main(String[] args) {
6 List<String> list = new ArrayList<>(); // 定义一个List集合
7 list.add("Java"); // 向list集合追加数据
8 list.add("Es6");
9 list.add(null);
10 list.add("Html5");
11 list.add(null);
12
13 System.out.println(list); // 输出集合中的数据
14 list.removeIf((e)-> { // 使用Predicate<T>进行去null处理
15 return e == null;}
16 );
17 System.out.println(list); // 输出删除之后的数据
18 }
19 }
程序运行结果如下:
[Java, Es6, null, Html5, null]
[Java, Es6, Html5]
例10-2中,先创建一个名字为list的ArrayList集合,然后向list中添加了5个元素,接着输出list集合中元素,然后又通过调用list的removeIf()方法,传递符合Predicate函数式接口的Lambda表达式。因为传递的元素是判断参数等于null,所以会删除list中值为null的元素,接下来在第17行代码输出list中的数据,结果发现值null的元素被删除了。
综上所述,函数式接口带给我们最大的好处就是:可以使用极简的lambda表达式实例化接口,这点在实际的开发中很有好处,往往一两行代码能够解决很复杂的场景需求。
注意:@FunctionalInterface注解加或不加对于接口是不是函数式接口没有影响,该注解只是提醒编译器去检查该接口是否仅包含一个抽象方法。
10.3 Lambda表达式的简化形式
Lambda表达式的核心原则是:只要可以推导,都可以省略,即可以根据上下文推导出来的内容,都可以省略书写,这样简化了代码,但潜在的问题是有可能会使代码可读性变差。本节介绍Lambda表达式的几种常用简化形式。
1.省略大括号
在Lambda表达式中,如果程序代码块只包含了一条语句,就可以省略大括号。标准格式和省略形式对比如下:
() -> {System.out.prnitln("一起来跟我学习JAVA的Lambda表达式");} // 标准格式
() -> System.out.prnitln("一起来跟我学习JAVA的Lambda表达式"); // 省略格式
2.省略参数类型
Lambda表达式可以根据上下文环境推断出参数类型,所以可以省略参数类型。标准格式和省略形式对比如下:
1 interface Calc { // 可计算接口
2 int calcInt(int x, int y); // 两个int类型参数
3 }
4 public static Calc calculate(char opr) {
5 Calc result;
6 if (opr == '+') {
7 result = (int x,int y) -> { // 标准格式
8 return x + y;
9 };
10 } else {
11 result = (x, y) -> { // 省略形式
12 return x - y;
13 };
14 }
15 return result;
16 }
3.省略小括号
当Lambda表达式中参数只有一个的时候,可以省略参数小括号。下面代码段使用到了第10.2节的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表达式后逐步使用简化形式。
10.4 访问变量
Lambda表达式中可以访问其外层作用域中定义的变量。例如,可以使用其外层类定义的实例或静态变量以及调用其外层类定义的方法,也可以显式或隐式地访问this变量。
10.4.1 访问成员变量
成员变量包括实例成员变量和静态成员变量。在Lambda表达式中,可以访问这些成员变量,此时的Lambda表达式与普通方法一样,可以读取成员变量,也可以修改成员变量。
接下来,通过案例演示Lambda表达式访问成员变量的功能,如例10-3所示。
例10-3 Demo1003.java
1 package com.aaa.p100401;
2
3 interface Calc {
4 int calcInt(int x, int y);
5 }
6 public class Demo1003{
7 private int count = 1; // 实例成员变量
8 private static int num = 2; // 静态成员变量
9 public static Calc add() { // 静态方法,进行加法运算
10 Calc result = (int x, int y) -> {
11 num++; // 访问静态成员变量,不能访问实例成员变量
12 int c = x + y + num; // 修改为x + y + num+this.count会报错
13 return c;
14 };
15 return result;
16 }
17 public Calc mul() { // 实例方法,进行乘法运算
18 Calc result = (int x, int y) -> {
19 num++; // 访问静态成员变量和实例成员变量
20 this.count++;
21 int c = x * y - num - this.count;
22 return c;
23 };
24 return result;
25 }
26
27 // 测试方法
28 public static void main(String[] args) {
29 System.out.println("静态方法,加法运算:" + add().calcInt(4, 3));
30 System.out.println("实例方法,减法运算:" + new Demo1003().mul().calcInt(4, 3));
31 }
32 }
程序的运行结果如下:
运行结果为:13
从程序运行结果来看,例10-3中声明了一个实例成员变量count和一个静态成员变量num。此外,还声明了静态方法add()和实例方法sub()。add()方法是静态方法,静态方法中不能访问实例成员变量,所以第12行代码的Lambda表达式中也不能访问实例成员变量,在代码“x+y+num”后加上this.count会报错。sub()方法是实例方法,实例方法中能够访问静态成员变量和实例成员变量,所以第18行代码的Lambda表达式中可以访问这些变量,当然实例方法和静态方法也可以访问,当访问实例成员变量或实例方法时可以使用this,在不与局部变量发生冲突情况下可以省略this。
10.4.2 捕获局部变量
对于成员变量的访问,Lambda表达式与普通方法没有区别,但是有时候Lambda表达式需要访问外部作用域代码中的变量,这些变量不是在函数体内定义的,是在Lambda表达式所处的上下文中定义的,这称为变量捕获或变量绑定。当Lambda表达式发生变量捕获时,系统编译器会将变量当成final的。因此,这些变量在声明时,可以不定义成final,并且Lambda表达式中不能修改那些捕获的变量。
接下来,通过案例演示如何捕获局部变量,如例10-4所示。
例10-4 Demo1004.java
1 package com.aaa.p100402;
2 import java.util.Comparator;
3
4 public class Demo1004 {
5 public static void main(String[] args) {
6 test();
7 }
8 static void test(){
9 Integer a = 222;
10 Comparator<Integer> com = (x, y) -> {
11 // 如果取消注释会报错,下面会详细解释
12 // a++;
13 return Integer.compare(x,y);
14 };
15 System.out.println("两个数字比较结果为:" + com.compare(22,33));
16 }
17 }
程序的运行结果如下:
两个数字比较结果为:-1
例10-4中,使用Comparator<Integer>比较接口,第8~14行代码为Lambda表达式的可以访问其外部域第9行代码,系统自动将第9行代码当成final类型的变量,此处显式加上final也可以,但是如果取消第11行代码注释,则程序编译报错,提示“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()内部不能有两个同名的变量,所以不能在Lambda表达式中定义已经存在的变量。
10.5 方法引用
方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。
当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。方法引用可以理解为lambda表达式的快捷写法,它比lambda表达式更加简洁,可读性更高,有更好的重用性。如果Lambda表达式的代码块只有一条代码,可以在代码块中使用方法引用 。方法引用本质上是一个Lambda表达式,使用的是双冒号"::"操作符。方法引用有4种形式,如表10.2所示。
表10.2 方法引用的四种形式
方法引用方式 |
示例 |
引用类的静态方法 |
ClassName::staticMethodName |
引用对象的实例方法 |
Object::instanceMethodName |
引用类的实例方法 |
ClassName::methodName |
引用构造方法 |
ClassName::new |
10.5.1 引用类的静态方法
引用类方法,其实就是引用类的静态方法,语法格式和示例如下:
格式: ClassName::staticMethodName
示例: String::valueOf
如果函数式接口的实现恰好可以通过调用一个静态方法来实现,那么就可以使用静态方法引用。此时,类是静态方法动作的发起者。假如 Lambda 表达式符合如下格式:
([变量1, 变量2, ...]) -> 类名.静态方法名([变量1, 变量2, ...])
可以简写成如下格式:
类名::静态方法名([变量1, 变量2, ...])
注意,这里静态方法名后面不需要加括号,也不用加参数,因为编译器都可以推断出来。具体参考下列等价代码格式:
String::valueOf等价于Lambda表达式 (str) -> String.valueOf(str)
Math::pow等价于Lambda表达式 (a,b) -> Math.pow(a, b);
接下来,通过案例来演示引用类的静态方法,如例10-5所示。
例10-5 Demo1005.java
1 package com.aaa.p100501;
1
2 interface Converter { // 接口负责将String类型转换为Integer
3 Integer change(String s);
4 }
5 public class Demo1005 {
6 public static void main(String[] args) {
7 Converter c1 = s -> Integer.parseInt(s); // Lambda表达式的写法
8 Integer v1 = c1.change("8319");
9 System.out.println("Lambda表达式输出:" + v1);
10 Converter c2 = Integer::parseInt; // 引用方法的写法;
11 Integer v2 = c2.change("8319");
12 System.out.println("方法引用输出:" + v1);
13 }
14 }
程序的运行结果如下:
Lambda表达式输出:8319
方法引用输出:8319
例10-5中,第3~5行定义了一个函数式接口Converter,在接口中定义了一个change()抽象方法,该方法负责将String参数转换为Integer类型。第8行代码使用Lambda表达式来创建一个Converter对象,由于其代码块只有一条语句,因此省略了大括号,并将这条语句的值作为返回值。第9行代码调用Converter的对象c1,因为c1对象是Lambda表达式创建的,c1的change()方法体就是Lambda表达式的代码块部分,因此第10行代码输出结果为8319。
由于上面的Lambda表达式的代码块只有一行调用类方法的代码,所以可以用类的方法引用来替换。第11行代码也就是调用Integer类的parseInt()方法来实现Converter函数式接口中唯一的抽象方法change(),当调用change()方法时,调用参数会传给Integer类的parseInt()类方法,因此第13行代码输出结果也为8319。
从程序运行结果可以看到,使用引用类方法和Lambda表达式,效果是一致的。可以说引用类方法是Lambda表达式的孪生兄弟,二者可以起到异曲同工的效果。
10.5.2 引用类的实例方法
引用对象的实例方法,其实就是引用类中的成员方法,语法格式和示例如下:
格式: Object::instanceMethodName
示例: String::substring
这和类调用静态方法不相同,动作的发起者是ClassName类所创建的任意一个对象,只不过在方法调用的时候需要将引用对象作为参数输入方法中,并且规定此对象一定要位于方法参数的第1个。如果 Lambda 表达式的“->”的右边要执行的表达式是调用的“->”的左边第1个参数的某个实例方法,并且从第2个参数开始(或无参)对应到该实例方法的参数列表时,就可以使用这种方法。假如Lambda 表达式符合如下格式:
(变量1[, 变量2, ...]) -> 变量1.实例方法([变量2, ...])
代码就可以简写成如下格式:
变量1对应的类名::实例方法名
接下来,通过实例来演示引用类的实例方法,如例10-6所示。
例10-6 Demo1006.java
1 package com.aaa.p100502;
1
2 interface MyString{
3 // 从开始位置startX开始到endX截取字符串
4 String mySubString(String s,int startX,int endX);
5 }
6 public class Demo1006 {
7 private static void useMyString(MyString myStr){
8 String str = myStr.mySubString("AAA软件教育欢迎您!",0,7);
9 System.out.println(str);
10 }
11 public static void main(String[] args) {
12 // 方式1:使用Lambda表达式的标准格式
13 useMyString((String s,int startX,int endX)->{
14 return s.substring(startX,endX);
15 });
16
17 useMyString((s,x,y)->s.substring(x,y)); // 方式2:使用Lambda表达式的简化格式
18 useMyString(String::substring); // 方式3:使用类的实例方法引用格式
19 }
20 }
程序的运行结果如下:
AAA软件教育
AAA软件教育
AAA软件教育
例10-6中,定义了一个接口MyString,包含一个抽象方法mySubString(),该方法负责根据String、int、int这3个参数生成一个String类型的返回值。第8~11行代码定义了一个静态的方法useMyString(),方法的参数是MyString类型的接口,因为Lambda表达式可以作为函数式接口的参数。在下面的main()方法中对静态方法useMyString()进行了3次调用,分别是Lambda表达式的标准格式、简化格式、方法引用格式。
从程序运行结果来看,从Lambda表达式到引用类的实例方法,结果都是一样的。Lambda表达式的简化格式是对标准格式的精简,这个在第10.3节已经详细讲解过。而引用类的实例方法格式更为简单,Lambda表达式被引用类的实例方法替代的时候,第1个调用参数作为substring()方法的调用者,剩下的参数会作为substring()实例方法的调用参数。
10.5.3 引用对象的实例方法
引用对象的实例方法,其实就引用类中的成员方法。这种语法与引用静态方法的语法类似,只不过这里使用对象引用而不是类名,语法格式和示例如下:
格式:Object::instanceMethodName
范例:"helloWorld":: toUpperCase
此时,对象是方法动作的发起者。当要执行的表达式是调用某个对象的方法,并且这个方法的参数列表和接口里抽象函数的参数列表一一对应时,就可以采用引用对象的方法的格式。
假如 Lambda 表达式符合如下格式:
([变量1, 变量2, ...]) -> 对象引用.方法名([变量1, 变量2, ...])
以简写成如下格式:
对象引用::方法名
例如,"helloWorld":toUpperCase()等价于Lambda表达式 () ->str.toUpperCase(),该实例方法引用就是调用了"helloWorld"的toUpperCase()实例方法来实现之前Lambda表达式中的抽象方法。
接下来,通过案例来演示引用对象的实例方法,如例10-7所示。
例10-7 Demo1007.java
1 package com.aaa.p100503;
2 import java.util.Arrays;
3 import java.util.List;
4
5 interface Converter{
6 Integer change(String from);
7 }
8 public class Demo1007 {
9 public static void main(String[] args) {
10 Converter c1 = from -> "www.3adazuo.cn".indexOf(from);
11 Integer value = c1.change("it");
12 System.out.println("3a在www.3adazuo.cn中的位置:" + value);
13 // 方法引用形式的输出
14 Converter c2 = "www.3adazuo.cn"::indexOf;
15 System.out.println("3a在www.3adazuo.cn中的位置:" + c2.change("3a"));
16 }
17 }
程序的运行结果如下:
3a在www.3adazuo.cn中的位置:4
3a在www.3adazuo.cn中的位置:4
例10-7中,定义了一个接口Converter,包含一个change()抽象方法,负责将String参数转换为Integer参数。第10行的Lambda表达式实现实现了该接口的change()方法,所以可以将表达式中代码块的值作为返回值。接着,第11行代码将调用c1对象的change()方法将字符串转换为整数了,由于c1对象是Lambda表达式创建的,change()方法的执行体就是Lambda表达式的代码块部分,所以第12行代码的输出结果返回“3a”在字符串“www.3adazuo.cn”中的位置4。第14行代码的实例方法引用,表示调用“www.3adazuo.cn”对象的indexOf()实例方法来实现Converter函数式接口中唯一的抽象方法change(),当调用该接口中的change()方法时,参数“3a”会传递给“www.3adazuo.cn”的indexOf()实例方法,相当于"www.3adazuo.cn".indexOf("3a"),同样输出结果为4。
10.5.4 引用构造方法
方法引用用来重用现有API的方法流程,JDK还提供了构造方法引用,用来重用现有API的对象构建流程。构造器引用同方法引用类似,不同的是在构造器引用中方法名是new,语法格式和示例如下:
格式:类名::new
范例:Customer::new。
对于拥有多个构造器的类,选择使用哪个构造器取决于上下文。方法引用有返回值类型,构造方法在语法上没有返回值类型。事实上,每个构造方法都会有返回值类型,也就是该类自身。
接下来,通过案例来演示引用构造方法,如例10-8所示。
例10-8 Demo1008.java
1 package com.aaa.p100504;
2 import java.util.ArrayList;
3 import java.util.*;
4 import java.util.function.Function;
5
6 class Customer {
7 String name;
8 public Customer(String name) {
9 this.name = name;
10 }
11 @Override
12 public String toString() {
13 return "Customer{" + "name='" + name + '\'' + '}';
14 }
15 }
16 public class Demo1008 {
17 static <P,R> List<R> map(List<P>list, Function<P,R> mapper){
18 List<R>mapped = new ArrayList<>();
19 for(int n = 0;n < list.size();n++){
20 mapped.add(mapper.apply(list.get(n)));
21 }
22 return mapped;
23 }
24 public static void main(String[] args) {
25 List<String>names = Arrays.asList("扁鹊","华佗");
26 List<Customer>customers = map(names,Customer::new);
27 for(Customer c:customers)
28 System.out.println(c);
29 }
30 }
程序的运行结果如下:
Customer{name='扁鹊'}
Customer{name='华佗'}
例10-8中,第6~15行代码定义了Customer 类,包含一个name成员变量,带一个参数的构造方法和toString()方法。第17~23行代码定义了一个map()方法,使用了Function函数式接口,该接口定义的一个apply(T t)抽象方法必须重写,该方法的作用是指定将获取的P类型数据转换为R类型,这里是先将数据放到mapped集合,然后转换为Customer的实例。第25行代码使用Arrays类的asList()方法对集合进行了赋值操作,第26行代码调用map()方法及其构造器引用将names集合转换为Customer的实例。第27行和第28行代码针对集合中的数据进行了遍历输出。
从程序运行结果来看,构造器引用在使用new关键字的时候,不需要使用类名和参数,由类内的方法直接调用Lambda表达式进行构造器方法调用即可。
知识点拨:如果有多个同名的重栽方法,编译器就会尝试从上下文中找出你指的那一个方法。例如,Math.max 方法有4个版本,参数类型分别是int、long、float、double。选择哪一个版本取决于Math::max 转换为哪个函数式接口的方法参数。类似于 lambda 表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。
10.6 Lambda表达式调用Arrays的类方法
Arrays类的一些方法需要Comparator、 XxxOperator、XxxFunction等接口的实例,这些接口都是函数式接口,因此可以使用Lambda表达式来调用Arrays的方法。
接下来,通过案例来演示使用Lambda表达式调用Arrays的类方法,如10-11所示。
例10-11 Demo1009.java
1 package com.aaa.p1006;
1 import java.util.Arrays;
1
1 public class Demo1009 {
1 public static void main(String[] args) {
1 String[] arr = new String[] { "CSDN","51CTO", "ITEye", "cnblogs" };
1 Arrays.parallelSort(arr,(s1, s2) -> s1.length() - s2.length());
1 System.out.println("排序:"+Arrays.toString(arr));
1 int[] intArray = new int[] {3, 9, 8, 0};
1 // left代表数组中前一个索引处的元素,计算第1个元素时left为1
1 // right代表数组中当前索引处的元素
1 Arrays.parallelPrefix(intArray, (left, right) -> left * right);
1 System.out.println("累积:" + Arrays.toString(intArray));
1 long[] longArray = new long[5];
1 // operand代表正在计算的元素索引
1 Arrays.parallelSetAll(longArray, operand -> operand * 5);
1 System.out.println("索引*5:" + Arrays.toString(longArray));
1 }
1 }
程序的运行结果如下:
排序:[CSDN, 51CTO, ITEye, CNBLOGS]
累积:[3, 27, 216, 0]
索引*5:[0, 5, 10, 15, 20]
通过程序运行结果可以发现,第7行代码的Lambda表达式的目标类型是Comparator,该接口指定了判断字符串大小的标准;第12行代码的Lambda表达式的目标类型是IntBinaryOperator,该对象将会根据前后两个元素来计算当前元素的值;第16行代码Lambda表达式的目标类型是IntToLongFunction,该对象将根据元素的索引来计算当前元素的值。通过本案例可以发现,Lambda表达式能够使程序更加简洁,代码更加简单。
10.7 本章小结
了解函数式编程思想是一种将操作与操作的实施过程进行分离的思想。
Lambda表达式是匿名类的一种简化,可以部分取代匿名类的作用。了解Lambda的使用场景,多用于匿名类的替代,方法引用等。
掌握Lambda表达式的基本语法,理解Lambda表达式的简化使用方式。
理解Lambda对于函数式接口的关系,如何作为该接口的目标类型使用,了解四类常见函数式接口。
理解Lambda表达式如何访问成员变量和捕获局部变量。
理解方法引用的概念,掌握方法引用的使用。
理解Lambda表达式调用Arrays的类方法。
了解Lambda表达式是后续集合章节,后继框架技术的基础。
10.8 理论试题与实践练习
1.填空题
1.1 Lambda表达式允许创建只有一个____________的接口。
1.2 函数式接口可以包含多个_______、________,但只能声明一个抽象方法。
1.3 方法引用和_________可以让Lambda表达式的代码块更加简单。
1.4 Lambda表达式重用API现有的方法,也称为_____________________。
2.选择题
2.1 方法引用不包括下面哪个( )
A.引用类方法 B.引用对象的实例方法
C.引用类的实例方法 D.迭代方法
2.2 下面哪个不是Arrays 函数式接口的实例( )
A.Comparator B.XXXFunction
C.XXXOperator D.list
3.编程题
3.1 请从键盘随机输入10个整数保存到List中,并使用Lambda表达式进行遍历。
3.2 定义一个函数式接口,使用Lambda表达式来实现其实例。
- 点赞
- 收藏
- 关注作者
评论(0)