1 Lambda表达式
Lambda表达式是Java8中的新特性,编码时,我们一般尽可能轻量级的将代码封装为数据,传统的解决方案是通过接口和实现类(匿名内部类)实现,这种方式存在语法冗余,this关键字,变量捕捉,数据控制等问题。而Lambda表达式则更为简化,它强调做什么,而不是以什么形式做。
1.1 Lambda表达式标准格式
Lambda省去面向对象的条条框框,格式由3个部分组成:
⼀些参数、⼀个箭头、⼀段代码。
Lambda表达式的标准格式为:
(参数类型 参数名称) -> { 代码语句 }
格式说明:
小括号内的语法与传统方法参数列表⼀致:无参数则留空;多个参数则用逗号分隔。
-> 是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本⼀致。
1.2 Lambda表达式的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。
但是使用时有几个问题需要特别注意:
1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。无论是JDK内置的Runnable 、 Comparator 接口还是自定义的接⼝,只有当接口中的抽象方法存在且唯⼀时,才可以使用Lambda。
2. 使用Lambda必须具有上下文推断。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有⼀个抽象方法的接口,称为“函数式接口”。
1.3 Lambda表达式的使用举例
下面举例演示 java.util.Comparator 接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象数组进行排序时,Arrays.sort 方法需要⼀个 Comparator 接口实例来指定排序的规则。假设有一个 Person 类,含有 String name 和 int age 两个成员变量:
public class Person {
private String name;
private int age;
// 省略构造器、toString⽅法与Getter Setter
}
传统写法:
如果使用传统的代码对 Person[] 数组进行排序,写法如下:
import java.util.Arrays;
import java.util.Comparator;
public class Demo06Comparator {
public static void main(String[] args) {
// 本来年龄乱序的对象数组
Person[] array = {
new Person("古⼒娜扎", 19),
new Person("迪丽热巴", 18),
new Person("⻢尔扎哈", 20) };
// 匿名内部类
Comparator<Person> comp = new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
};
Arrays.sort(array, comp); // 第⼆个参数为排序规则,即Comparator接⼝实例
for (Person person : array) {
System.out.println(person);
}
}
}
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中 Comparator 接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。
代码分析:
下面我们来搞清楚上述代码真正要做什么事情。
为了排序, Arrays.sort 方法需要排序规则,即 Comparator 接口的实例,抽象方法 compare 是关键; 为了指定 compare 的方法体,不得不需要 Comparator 接口的实现类;为了省去定义⼀个 ComparatorImpl 实现类的麻烦,不得不使用匿名内部类;必须覆盖重写抽象 compare 方法,所以方法名称、方法参数、方法返回值不得不再写⼀遍,且不能写错;实际上,只有参数和方法体才是关键。
Lambda写法:
import java.util.Arrays;
public class Demo07ComparatorLambda {
public static void main(String[] args) {
Person[] array = {
new Person("古⼒娜扎", 19),
new Person("迪丽热巴", 18),
new Person("⻢尔扎哈", 20) };
Arrays.sort(array, (Person a, Person b) -> {
return a.getAge() - b.getAge();
});
for (Person person : array) {
System.out.println(person);
}
}
}
2 方法引用
在使用Lambda表达式的时候,我们实际上传递进去的代码就是⼀种解决方案:拿什么参数做什么操作。那么考虑⼀种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?
2.1 方法引用符
双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用。
如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
具体例子下面会进行说明。
2.2 冗余的Lambda场景
来看一个简单的函数式接口以应用Lambda表达式:
@FunctionalInterface
public interface Printable {
void print(String str);
}
在 Printable 接口当中唯一的抽象方法 print 接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda来使用它的代码很简单:
public class Demo01PrintSimple {
private static void printString(Printable data) {
data.print("Hello, World!");
}
public static void main(String[] args) {
printString(s -> System.out.println(s));
}
}
其中 printString 方法只管调用 Printable 接口的 print 方法,而并不管 print 方法的具体实现逻辑会将字符串打印到什么地方去。而 main 方法通过Lambda表达式指定了函数式接口 Printable 的具体操作方案为:拿到String(类型可推导,所以可省略)数据后,在控制台中输出它。
问题分析:
这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现, 即System.out 对象中的 println(String) 方法。
既然Lambda希望做的事情就是调用 println(String) 方法,那何必自己手动调用呢?
2.3 用方法引用改进代码
能否省去Lambda的语法格式(尽管它已经相当简洁)呢?
只要“引用”过去就好了:
public class Demo02PrintRef {
private static void printString(Printable data) {
data.print("Hello, World!");
}
public static void main(String[] args) {
printString(System.out::println);
}
}
再次注意:请关注其中的双冒号 :: 写法,这被称为“方法引用”,而双冒号是⼀种新的语法。
语义分析:
上例中,System.out 对象中有⼀个重载的 println(String) 方法恰好就是我们所需要的。
那么对于 printString 方法的函数式接口参数,对比下面两种写法,完全等效:
Lambda表达式写法: s -> System.out.println(s) ;
方法引用写法: System.out::println 。
第⼀种语义是指:拿到参数之后经Lambda之⼿,继而传递给 System.out.println 方法去处理。
第⼆种等效写法的语义是指:直接让 System.out 中的 println 方法来取代Lambda。
两种写法的执行效果完全⼀样,而第⼆种方法引用的写法复用了已有方案,更加简洁。
注:Lambda 中传递的参数一定是方法引用中的那个方法可以接收的类型,否则会抛出异常。
推导与省略:
如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式 -- 它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。
函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。
下面这段代码将会调用 println 方法的不同重载形式,将函数式接口改为int类型的参数:
@FunctionalInterface
public interface PrintableInteger {
void print(int str);
}
由于上下文变了之后可以自动推导出唯⼀对应的匹配重载,所以方法引用没有任何变化:
public class Demo03PrintOverload {
private static void printInteger(PrintableInteger data) {
data.print(1024);
}
public static void main(String[] args) {
printInteger(System.out::println);
}
}
这次方法引用将会自动匹配到 println(int) 的重载形式。
2.4 方法引用的各种用法
1.对象引用成员方法
2.类引用静态方法
3.引用构造方法
4.引用数组的构造器: int[] arr = new int[10] 。
5.this引用成员方法: this是使用在lambda表达式中, 必须出现类的成员方法中。
6.super引用父类的方法: super, 必须出现在子类的成员方法里。