文章目录
Lambda表达式可以理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。Lambda表达式由参数、箭头和主体组成。
- 如果lambda代码块主体无法用一个表达式表示,就要用{}包裹代码并明确使用return语句;
- 如果lambda表达式没有参数,可以提供一对空的小括号,如同不带参的方法那样;
- 如果一个lambda表达式的参数类型是可以被推导的,那么可以省略它们的类型;
函数式接口
函数式接口的转换是使用lambda表达式能做的唯一一件事。函数式接口就是只定义一个抽象方法的接口,但是可以有多个非抽象方法,这样的接口可以被隐式转换为lambda表达式。
java1.8使我们能够通过使用default
关键字向接口添加非抽象方法实现,此功能也称为虚拟扩展法。
interface Formula{
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
实现该接口的类只要实现抽象方法就可以直接使用默认方法;也可以直接通过接口创建对象,然后实现接口的默认方法。
public class Main {
public static void main(String[] args) {
// 通过匿名内部类方式访问接口
Formula formula = new Formula() {
@Override
public double calculate(int a) {
return sqrt(a * 100);
}
};
System.out.println(formula.calculate(100)); // 100.0
System.out.println(formula.sqrt(16)); // 4.0
}
}
注:上述通过匿名内部类方式访问接口,我们可以这样理解:一个内部类实现了接口里的抽象方法并且返回一个内部类对象,然后让接口的引用指向这个对象。
java1.8还增加了一种特殊的注解@FuntionalInterface
,但是这个注解通常不是必须的。只要接口只包含一个抽象方法,虚拟机会自动判断该接口为函数式接口。
@FunctionalInterface
public interface Converter<F, T> {
T convert(F from);
}
Lambda实践:环绕执行模式
下面是一段代码,从一个文件中读取一行所需的文件内容:
public static String processFile() throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
return br.readLine();
}
}
行为参数化
这段代码的局限性在于只能读取文件的第一行,当要执行processFile方法对文件执行不同的操作时,就要把processFile的行为参数化:根据传递给它的参数不同,其行为也会不同。Lambda可以传递行为,把行为传递给processFile以便利用BufferedReader执行不同的行为。
String result = processFile( (BufferedReader br) -> br.readLine() + br.readLine() );
使用函数式接口来传递行为
Lambda表达式仅可用于上下文是函数式接口的情况,这时就需要创建一个能匹配BufferedReader -> String
,还可以抛出IOException
异常的接口。
@FuctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
将这个接口作为新的processFile方法的参数:
public static String processFile(BufferedReaderProcessor p) throws IOException{
...
}
执行一个行为
在processFile主体内,对得到的BufferedReaderProcessor对象调用process方法执行处理:
public static String processFile(BufferedReaderProcessor p) throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
return p.process(br);
}
}
传递Lambda
现在就可以通过传递不同的Lambda重用processFile方法,并以不同的方式处理文件:
String oneLine = processFile( (BufferedReader br) -> br.readLine() ); //处理一行
String twoLines = processFile( (BufferedReader br) -> br.readLine() + br.readLine() ); //处理两行
下图总结了所采取的使processFile方法更灵活的四个步骤。
使用函数式接口
函数式接口的抽象方法的签名称为函数描述符,Java API中常见的函数式接口有:Comparable、Runnable和Callable。Java8中java.util.function
包引入了几个新的函数式接口:Predicate、Consumer和Function。
Predicate
java.util.function.Predicate<T>
接口定义了一个test抽象方法,它接受泛型T对象并返回一个boolean。在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。
Consumer
java.util.function.Consumer<T>
定义了一个accept的抽象方法,它接受泛型T对象而没有返回值(void)。你可以用它来创建一个forEach方法,接受一个Integer的列表,并对其中每个元素执行操作。
Function
java.util.function.Function<T, R>
接口定义了一个apply方法,它接受一个泛型T对象并返回一个泛型R对象。如果你需要将输入对象的信息映射到输出,就可以使用这个接口。
类型检查、类型推断以及限制
类型检查:Lambda的类型是从使用Lambda的上下文推断出来的。
上述类型检查可以分为以下步骤:
- filter方法声明第二个参数为Predicate< Apple >,因此将T绑定到Apple;
- 该函数式接口定义了一个test抽象方法,test方法描述了一个函数描述符。它可以接受一个Apple对象,并返回一个boolean对象;
类型推断:Java编译器会从上下文推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名。从而,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。
有限制的局部变量:Lambda表达式也允许使用自由变量,就像匿名类一样。但局部变量必须显式声明为final,或事实上就是一个final变量。
方法引用
方法引用就是让你根据已有的方法实现来创建Lambda表达式,可被看作是仅仅调用特定方法的Lambda的一种快捷写法。
方法引用主要有三种:
- 指向静态方法的方法引用;(例如Integer的parseInt方法,写作Integer::parseInt)
- 指向任意类型实例方法的引用;(例如String的length方法,写作String::length)
- 指向现有对象的实例方法的方法引用;(假设局部变量expensiveTransaction存放Transaction类型对象,它支持getValue方法,就可以写expensiveTransaction::getValue)
构造函数引用
对于一个现有构造函数,可以利用它的名称和关键字new来创建它的一个引用:Classname::new。
无参构造函数
只有一个参数的构造函数
具有两个参数的构造函数
Lambda和方法引用实战
下面将一步步展示如何用lambda表达式来对一个Apple列表进行排序:
- 首先用List的sort(Comparator<? super E> c)方法,它需要一个Comparator对象来比较Apple。
public class AppleComparator implements Comparator<Apple>{
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
}
- 可以使用匿名内部类进行改进,而不是实现一个Comparator却只实例化一次:
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
- 也可以使用Lambda表达式来传递Comparator的函数描述符
(T, T) -> int
:
inventory.sort( (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()) );
- Comparator具有一个comparing的静态辅助方法,可以接受一个Function来提取Comparable键值,并生成一个Comparator对象:
Comparator<Apple> c = Comparator.comparing( (a) -> a.getWeight() );
inventory.sort(comparing( (a) -> a.getWeight() ) );
- 使用方法引用:
inventory.sort( comparing(Apple::getWeight() );
复合Lambda表达式
比较器复合
谓词复合
谓词接口包括三个方法:negate、and和or,让你可以重用已有的Predicate来创建更复杂的谓词。
也可把两个lambda用and方法组合起来:
进一步组合谓词:
函数复合
最后,还可以把Function接口所代表的Lambda表达式复合起来,Function接口为此提供了andThen和compose两个默认方法,它们都会返回Function的一个实例。