Lambda表达式
利用行为参数化这个概念,就可以编写更为灵活且可重复使用的代码。但同时,使用匿名类来表示不同的行为并不令人满意。Java 8引入了Lambda表达式来解决这个问题。它使你以一种很简洁的表示一个行为或传递代码。
可以将Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
- 匿名 - 因为它不像普通的方法一样有一个明确的名称。
- 函数 - 说它是函数是因为Lambda函数不像方法那样属于某个特定的类,但和方法要一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
- 传递 - Lambda表达式可以作为参数传递给方法或存储在变量中。
- 简洁 - 无需像匿名类那样写很多模板代码
使用Lambda的最终结果就是你的代码变得更清晰、灵活。打比方,利用Lambda表达式,可以更为简洁地自定义一个Comparator对象。
对比以下两段代码:
Comparator <Apple> byWeight = new Comparator<Apple>(){
public int compare(Apple a1,Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
代码看起来更清晰了。基本上只传递了真正需要的代码(compare方法的主体)。
Lambda表达式由三个部分,如图 Lambda表达式由参数、箭头和主体组成 所示
- 参数列表 - 这里它采用了Comparator中compare方法的参数,两个Apple。
- 箭头 - 箭头
->
把参数列表与Lambda主体分隔开。 - Lambda主体 - 比较两个Apple的重量。表达式就是Lambda的返回值。
Lambda 的基本语法是:
(parameters) -> expression
或 (parameters) -> { statements; }
函数式接口
之前的Predicate<T>就是一个函数式接口,因为Predicate仅仅定义了一个抽象方法:
public interface Predicate<T>{
boolean test(T t);
}
简而言之,函数式接口就是只定义一个抽象方法的接口。Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。
函数式接口的抽象方法的签名
把Lambda付诸实践:环绕执行模式
要从一个文件中读取一行所需的模板代码:
public static String processFile() throws IOException{
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))){
return br.readLine();
}
}
现在这段代码的局限性在于只能读文件的第一行,如果想要返回头两行,甚至是返回使用最频繁的词。这时需要把processFile的行为参数化。
需要一个接收BufferedReader并返回String的Lambda。下面是从BufferedReader中打印两行的写法:
String result = processFile(BufferedReader br) -> br.readLine() + br.readLine());
现在需要创建一个能匹配BufferedReader -> String,还可以抛出IOException异常的接口:
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
@FunctionalInterface 是什么?
这个标注用于表示该接口会设计成一个函数式接口。如果用@FunctionalInterface
定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。
使用它不是必须的,但是使用它是比较好的做法。
现在就可以把这个接口作为新的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重用processFile方法,并以不用的方式处理文件了。
处理一行:
String oneLine =
processFile((BufferedReader br) -> br.readLine());
处理两行:
String twoLines =
processFile((BufferedReader br) -> br.readLine() + br.readLine());
完整代码如下:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class test{
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("c:/tmp/data.txt"))){
return p.process(br);
}
}
public static void main(String[] args) throws IOException {
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
System.out.println(oneLine);
System.out.println(twoLines);
}
}
现在已经能成功的利用函数式接口来传递Lambda。
使用函数式接口
函数接口定义且只定义了一个抽象方法。函数式接口的抽象方法的签名称为函数描述符。因此,为了应用不同的Lambda表达式,需要一套能够描述常见函数描述符的函数式接口。
Java 8在java.util.function包中加入了许多新的函数式接口,你可以重用它来传递多个不同的Lambda。
Predicate
java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。在需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。
public interface Predicate<T> {
boolean test(T t);
}
例如:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> results = new ArrayList<>();
for (T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T对象,没有返回。
例如:
@FunctionalInterface
public interface Comsumer<T> {
void accept(T t);
}
public static <T> void forEach(List<T> list, Predicate<T> c){
for (T i: list){
c.accept(i);
}
}
forEach(
Arrays.asList(1,2,3,4,5),
(Interger i) -> System.ou.println(i)
);
Function
java.util.function.Function<T, R>定义了一个名叫apply的抽象方法,它接受一个泛型T对象,并返回一个泛型R的对象。
下面将创建一个map方法,以将一个String列表映射到包含每个String长度的Interger列表:
@FunctionalInterface
public interface Function<T, R> {
R accept(T t);
}
public static <T, R> List<R> map(List<T> list, Function<T, R> f){
List<R> result = new ArrayList<>();
for (T s: list){
result.add(f.apply(s));
}
return result;
}
List<Integer> l =map(Arrays.asList("lambdas","in","action"), (String s) -> s.length());
原始类型特化
Java的类型有两种: 引用类型 和 原始类型 。但是泛型只能绑定到引用类型。这是由于泛型内部的实现方式造成的。因此Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫装箱(boxing)。相反的操作便叫拆箱(unboxing)。装箱和拆箱操作是可以由自动装箱机制来自动完成的。但是这在性能方面要付出代价,装箱后的值需要更多的内存并且需要额外的内存。
Java 8为原始类型带来了一个专门的版本,用于在输入和输出都是原始类型时避免自动装箱的操作:
public interface IntPredicate{
boolean test(int t);
}
无装箱:
IntPredicate evenNumbers = (int i) -> i%2 ==0;
evenNumbers.test(1000);
装箱:
Predicate<Integer> oddNumbers = (Integer i) -> i%2 == 1;
oddNumbers.test(1000);
类型检查、类型推断以及限制
Lambda本身并不包含它在实现哪个函数式接口的信息。为了全面了解Lambda表达式应该知道Lambda的实际类型是什么。
类型检查
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(例如接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。
同样的Lambda,不同的函数式接口
有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来。
类型推断
可以进一步简化你的代码。编译器会从上下文(目标类型)推断出痛什么函数式接口来配合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());
使用局部变量
之前介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda。
例如:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
Lambda捕获了portNumber变量。但关于对这些变量可以做什么有一些限制。Lambda可以没有限制的捕获实例变量和静态变量(也就是在其主体中引用)。但是局部变量必须显示声明为final(或事实上是final)。
下面的代码无法编译,因为portNumber变量被赋值了两次:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
实例变量和局部变量背后的实现有一个关键的不同。实例变量都存储在堆中,而局部变量则保存在栈上。Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后去访问该变量,这回引发造成线程不安全的新的可能性。
方法引用
方法引用可以重复使用现有的方法定义,并像Lambda一样传递它们。
下面是用方法引用写的一个排序的例子:
先前:
invenstory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
之后(使用方法引用和 java.util.Comparator.comparing
):
inventory.sort(comparing(Apple::getWeight));
方法引用可以被看做仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。
方法引用就是让你根据已有的方法实现来创建Lambda表达式。
当你需要使用方法引用时,目标引用放在分隔符 ::
前,方法的名称放在后面。例如 Apple::getWeight
就是引用了Apple类中定义的方法getWeight。
方法引用也可以看做针对仅仅设计单一方法的Lambda的语法糖。
构建方法引用
方法引用主要有三类。
- 指向静态方法的方法引用(Integer::parseInt)
- 指向任意类型实例方法的方法引用(String::length)
- 指向现有对象的实例方法的方法引用(Transaction::getValue)
第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。
例如 (String s) -> s.toUpperCase()
可以写作 String::toUpperCase
。
第三种方法引用指的是,你在Lambda中调用一个已经存在的外部对象的方法。例如,Lambda表达式 () -> expenssiveTransaction.getValue()
可以写作 expensiveTransaction::getValue
。
方法引用不需要括号,是因为没有实际调用这个方法。
构造函数引用
可以利用现有构造函数的名称和关键字来创建它的一个引用 ClassName:new
例如:
List<Integer> weight = Arrays.asList(7,3,4,10);
List<Apple> apples = map(weights, Apple::new);
public static List<Apple> map(List<Integer> List, Function<Integer, Apple> f){
List<Apple> result = new ArrayList<>();
for(Integer e: list){
result.add(f.apply(e))
}
return result;
}
Lambda和方法引用实战(用不同的排序策略给一个Apple列表排序)
第1步:传递代码
Java API已经提供了一个List可用的sort方法,要如何把排序策略传递给sort呢?sort方法的签名样子如下:
void 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());
}
}
inventory.sort(new AppleComparator());
第2步:使用匿名类改进
inventory.sort(new AppleComparator<Apple>(){
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
使用匿名类的意义仅仅在于不用为了只实例化一次而实现一个Comparator。
第3步:使用Lambda表达式
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
或
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
Comparator有一个叫做comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象:
import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));
第4步:使用方法引用
inventory(comparing(Apple::getWeight));
复合Lambda表达式的有用方法
比较器复合
对于:
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
1.逆序
想要按重量递减排序怎么办?不需要建立一个新的Comparator的实例。接口有个默认方法 reverse
可以使给定的比较器逆序。因此仍然可以用之前的那个比较器:
inventory.sort(comparing(Apple::getWeight).reversed());
2.比较器链
如果两个苹果一样重怎么办,哪个苹果应该排在前面?这时候可能需要再提供一个Comparator来进一步比较。
thenComparing
就是做这个用的。它接受一个函数作为参数(与comparing方法一样),如果两个对象用第一个Comparator比较之后是一样的,就提供第二个Comparator:
inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));
谓词复合
谓词接口包括三个方法:negate、and 和 or。
- negate
可以使用negate方法来返回一个Predicate的非,比如苹果不是红色:
Predicate<Apple> notRedApple = redApple.negate()
- and
可以用and方法将两个Lambda组合起来:
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
- or
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150).or(a -> "green".equals(a.getcolor()));
函数复合
还可以把Function接口所代表的Lambda表达式复合起来。Function接口有两个默认方法:andThen和 compose。它们都会返回Function的一个实例。
- andThen 方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。
比如函数f给数字加1,另一个函数给数字乘2:
Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.andThen(g);
int result = h.apply(1);
在数学上意味着g(f(x))。
- compose 方法先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。
Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.compose(g);
int result = h.apply(1);
在数学上意味着f(g(x))。