什么是Lambda表达式?
Lambda表达式可以理解为一种特殊的匿名函数,它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
Lambda的基本语法:
(parameters) -> expression
// 或者
(parameters) -> {statements; }
// 其中expression表示表达式,statement表示语句
在哪里使用Lambda表达式?
只有在接受函数式接口的地方才可以使用Lambda表达式(函数式接口就是仅仅声明一个抽象方法的接口)。Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。
任何内置的函数式接口都不允许抛出受检异常( checked exception)。如果需要Lambda表达式抛出异常,有两种方法:1)定义一个自己的函数式接口,并声明受检异常;2)把Lambda表达式包在一个try/catch块中。示例如下:
// 第一种方法:
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader b) -> br.readLine();
// 第二种方法:
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
} catch (IOException e) {
throw new RuntimeException(e);
}
};
只有在函数式接口的地方才能使用Lambda表达式,那如何判断Lambda表达式的类型和函数式接口的类型(称为目标类型)是否一致呢?
类型推断:函数描述符可以通过目标类型来得到,也就是说使用了哪个函数式接口,就知道了应该用哪种类型的参数和返回值,因此Java编译器可以根据函数描述符来推断出适合Lambda的签名。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。示例如下:
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 省略参数类型
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
// 这里的目标类型是Comparator,
// 它的抽象方法compare的函数描述符是(Apple, Apple -> int),因此就可以推断出Lambda表达式的参数类型
函数式接口是指仅仅包含一个抽象方法,但是可以有多个非抽象方法(即默认方法)的接口。
1)Predicate
java.util.function.Predicate
接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。
2)Consumer
java.util.function.Consumer
定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。
3)Function
java.util.function.Function
接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。
使用示例:
public class BuiltInFunctionalInterface {
// Predicate函数式接口练习,函数描述符:T -> boolean
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for (T t : list) {
if (p.test(t)) {
result.add(t);
}
}
return result;
}
// Consumer函数式接口练习,函数描述符:T -> void
public static <T> void forEach(List<T> list, Consumer<T> c) {
for (T t : list) {
c.accept(t);
}
}
// Function函数式接口,函数描述符:T -> R
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for (T t : list) {
result.add(f.apply(t));
}
return result;
}
public static void main(String[] args) {
List<String> listOfStrings = Arrays.asList("Apple", "", "Banana");
List<String> nonEmpty = filter(listOfStrings, (String s) -> !s.isEmpty());
for (String s : nonEmpty) {
System.out.println(s);
}
System.out.println("======");
List<Integer> listOfNums = Arrays.asList(1,2,3,4,5);
forEach(listOfNums, (Integer i) -> System.out.println(i));
System.out.println("======");
List<String> listOfStr = Arrays.asList("Java", "C++", "Scala");
List<Integer> result = map(listOfStr, (String str) -> str.length());
for (Integer i : result) {
System.out.println(i);
}
}
}
方法引用只是Lambda表达式的一种快捷写法,因此必须先理解Lambda表达式,才能用好方法引用。
方法引用的基本思想:有些Lambda表达式中只是调用了某个方法,比如(Apple a) -> a.getWeight()
,如果直接使用方法名字(getWeight方法)来调用它,而不是使用表达式或语句来描述如何调用它,就会使得代码可读性更好,上述Lambda表达式等效的方法引用就是Apple::getWeight
,直接通过方法名来表示获得苹果的重量,可读性较好。
基本语法:目标引用::方法名称
,注意:方法名称不需要括号,因为没有实际调用这个方法。
方法引用主要有三类:
第二种和第三种的区分:第二种方法引用表示引用一个对象的方法,而这个对象是Lambda的一个参数。例如,Lambda表达式(String s) -> s.toUpperCase()
可以写成String::toUpperCase
。第三种方法引用表示在Lambda表达式中调用一个已经存在的外部对象中的方法。
Lambda表达式与三种引用的对应关系:
// 第一种
(args) -> ClassName.staticMethod(args)
ClassName::staticMethod
// 第二种
(arg0, rest) -> arg0.instanceMethod(rest) // arg0是ClassName类型的
ClassName::instanceMethod
// 第三种
(args) -> expr.instanceMethod(args)
expr::instanceMethod
对于一个现有构造函数,可以利用它的名称和关键字new来创建它的一个引用:ClassName::new
。
对于无参构造函数,适用于Supplier函数式接口:
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get(); // 调用Supplier的get方法将产生一个新的Apple
对于一个参数的构造函数Apple(Integet weight)
,适用于Function接口:
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apple(50);
对于两个参数的构造函数Apple(String color, Integet weight)
,适用于BiFunction接口:
BiFunction<String, Integer, Apple> c3 = Apple::new;
Apple a3 = c3.apple("red", 50);
为什么构造函数参数数量不同,需要对应不同的函数式接口?
因为方法引用本质上就是Lambda表达式,所以Lambda表达式的签名和函数式接口的函数描述符必须要匹配。
上面的接口Supplier、Function、BiFunction都是Java内置的接口,所以可以直接使用。对于具有三个参数的构造函数,比如Color(int, int, int)
,语言本身没有没有提供与之匹配的函数式接口,此时可以自己创建一个,如下:
public interface TriFunction<T, U, V, R> {
R apple(T t, U u, V v);
}
// 使用构造函数引用
TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;
参考资料:《Java 8实战》第3章