被Lambda表达式、方法引用(method reference)、Supplier接口这几个概念弄得有点晕,参考上面文档,简单梳理一下。
粗略讲:Lambda表达式帮你将函数看作一个参数传入方法中;方法引用是其中一种特殊情况;Supplier是java.util.function包众多接口一种,为前两者提供工具。
一、Lambda表达式
1.引入
以代码举例:有个社交网络应用,需要设计一个功能:用户选择范围,然后打印出满足条件的联系人。下面是联系人Person类:
public class Person {//联系人
public enum Sex {
MALE, FEMALE
}
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
// ...
}
public void printPerson() {
// ...
}
}
使用匿名类
常用的一种方法是匿名类,实现功能的方法是printPersons(),因为范围可能会经常变,所以抽出来做一个接口CheckPerson,不同的范围对应不同实现:
interface CheckPerson {//范围接口
boolean test(Person p);
}
public static void printPersons(//打印出满足条件的联系人
List<Person> list, CheckPerson tester) {
for (Person p : list) {
if (tester.test(p)) {
p.printPerson();
}
}
}
printPersons(//具体实现,范围是18-25岁的男性
list,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);
这样很灵活,范围的变化不会影响到printPersons();缺点是代码有点臃肿。
使用Lambda表达式
CheckPerson接口只有一个抽象方法test,我们把这样只有一个抽象方法(可以有一般方法和静态方法)的接口称作函数接口(functional interface)。我们可以使用Lambda表达式来省略方法名称,让代码更简洁:
printPersons(
list,
(Person p) -> p.getGender() == Person.Sex.MALE //将匿名类替换成Lambda表达式
&& p.getAge() >= 18
&& p.getAge() <= 25
);
其实还可以再简洁,java提供了一套标准接口 java.util.function,替代自定义的CheckPersons。至于选择哪一个,则根据Lambda表达式的参数和返回值,例如上面只有一个Object类型参数p,返回值是boolean类型,所以匹配Predicate<T>,替换后代码更紧凑:
import java.util.function.Predicate;
public static void printPersons(
List<Person> list, Predicate<Person> tester) {//使用标准接口Predicate替换自定义CheckPerson
for (Person p : list) {
if (tester.test(p)) {
p.printPerson();
}
}
}
printPersons(
list,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);
2.语法
Lambda表达式包括三部分:
- 参数:由逗号分隔,包含在括号()里,可以省略类型。如果只有一个参数 也可以省略括号,例如上面的 p。
- 箭头 ->
- 主体:可以是表达式(expression),如上面的例子,Java runtime会识别并帮你返回值。也可以是大括号{ }包围的代码块(statement block),上面例子写成这种形式:
p -> { //一旦使用return,必须用大括号包住
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
下面是两个参数的例子:
import java.util.funtion.IntBinaryOperator;//接受两个int型参数,返回int型结果
public class Calculator {
public int operate(int a, int b, IntBinaryOperator op) {
return op.applyAsInt(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
//两个参数
IntBinaryOperator addition = (a, b) -> a + b;
IntBinaryOperator subtraction = (a, b) -> a - b;
System.out.println("1 + 2 = " + myApp.operate(1, 2, addition));
System.out.println("2 - 1 = " + myApp.operate(2, 1, subtraction));
}
}
3.使用时机和范围
时机:除了上面函数接口的情况,当你想将一段代码(a single unit of behavior)传给其他代码时,可以用Lambda对其概括。
范围:Lambda表达式作为参数,它的类型是什么?其实是根据上下文(context,situation)来决定的:当Java runtime调用 printPersons( List<Person> list, CheckPerson tester) 时,期望是CheckPerson类型,所以这个Lambda表达式就是CheckPerson类型,而调用 printPersons( List<Person> list, Predicate<Person> tester) 时,则Predicate<Person>类型,方法所期待的类型叫目标类型(target type), java.util.function就是为Lambda提供目标类型的。
所以Lambda表达式只能在有目标类型的情况下使用:
- Variable declarations 变量声明
- Assignments 赋值操作
- Return statements 返回值return
- Array initializers 数组初始化
- Method or constructor arguments 方法/构造器参数
- Lambda expression bodies Lambda表达式主体?
- Conditional expressions,
?: 条件表达式
- Cast expressions 类型转换
二、方法引用(method reference)
当Lambda表达式引用一个已经存在的方法,可以用双冒号::直接引用方法名,分为四种情况:
- 静态方法:ClassName::staticMethodName
- 一般方法:ObjectName::methodName
- 任意对象的方法:ClassName::methodName
- 构造方法: ClassName::new
第三种情况不太好懂,文档用的例子是:
String[] stringArray = { "Barbara", "James", "Mary", "John",
"Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
我的理解,数组中的字符串对象没有名称,无法用ObjectName直接引用,所以用ClassName。