原文链接 译者:hyssop
采用Lambda表达式
本章,你将学习到如何采用JAVA8的重要特性Lambda表达式。首先,你要了解“行为参数”这种模式。该模式能够使你写出来的代码适应需求变化。然后,你将看到该模式如何使得Lambda表达式的使用与以往比变得更加简洁。然后,你将学习如何精确地定位Lambda表达式的使用场景和使用方式。你也将了解JAVA8的另一个特性-方法参数,它能使你的代码更简洁更易读。带着所有这些新知识实战一个重构代码的例子。最后,你也将学习到如何使用Lambda表达式和方法参数。
为什么使用Lambda表达式
将Lambda表达式引入JAVA中的动机源于一个叫“行为参数”的模式。这种模式能够解决需求变化带来的问题,使代码变得更加灵活。在JAVA8之前,参数模式十分啰嗦。Lambda表达式通过精简的方式使用行为模式克服了这个缺点。举个例子,如果你需要找大于一定金额的发票。你可能写这样一个indInvoicesGreaterThanAmount方法:
List findInvoicesGreaterThanAmount(List invoi
ces, double amount) {
List result = new ArrayList<>();
for(Invoice inv: invoices) {
if(inv.getAmount() > amount) {
result.add(inv);
}
}
return result;
}
这个方法确实足够简单。可是如果你还需要找小于一定金额的发票呢?或者更糟糕的是,你需要从一个指定商户中找发票,并且也需要找一定数额呢?这时你需要一个方式来参数化指定条件下的过滤行为。下面我们将定义一个接口InvoicePredicate来描述条件,使用该接口重构上面的方法。
接口定义
interface InvoicePredicate {
boolean test(invoice inv);
}
重构方法
List<Invoice> findInvoices(List<Invoice> invoices, InvoicePredi
cate p) {
List<Invoice> result = new ArrayList<>();
for(Invoice inv: invoices) {
if(p.test(inv)) {
result.add(inv);
}
}
return result;
}
使用这段代码,你能够通过增加一个Invoice对象解决需求变化带来的问题。你只需要创建一个不同的InvoicePredicate 对象,将其传入方法findInvoices中。换句话说,你已经参数化了findInvoices行为。不好的是,使用这个新方法引入了额外的冗余,来看下面的代码:
List<Invoice> expensiveInvoicesFromOracle
= findInvoices(invoices, new InvoicePredicate() {
public test(Invoice inv) {
return inv.getAmount() > 10_000
&& inv.getCustomer() == Customer.ORACLE;
}
});
换句话说,代码变得更灵活的同时可读性变差了。最理想的状态是,代码灵活性和可读性兼备。Lambda表达式的引入能够做到这一点。通过使用它重构上面的代码如下:
List<Invoice> expensiveInvoicesFromOracle
= findInvoices(invoices, inv ->
inv.getAmount() > 10_000
&& inv.getCustomer() ==
Customer.ORACLE);
Lambda表达式的定义
现在知道了为什么需要在代码中引入Lambda表达式,也是时候精准的了解下Lambda表达式的定义。简单的来讲,lambda表达式是一个能够被传递的匿名函数,我们仔细看看这个定义:
匿名
Lambda表达式是匿名的,因为它没有像普通方法那样有一个明确的名称。它有点儿像匿名类,因为两者都没有明确的名称。
函数
一个 Lambda表达式像一个方法,它包含一串参数、一个体、一个返回类型和一串可能抛出的错误。和方法不同的是,它没有被声明为特殊类的一部分。
传递
一个 Lambda表达式能够作为一个方法的参数被传递,也能够作为一个结果被返回。
Lambda表达式语法
在写lambda表达式之前,需要知道它的语法。在本书中已经出现过lambda表达式:
Runnable r = () -> System.out.println("Hi");
FileFilter isXml = (File f) -> f.getName().endsWith(“.xml”);
这两个lambda表达式有三个部分:
- 一串参数,比如 (File f)
- 一个有两个字符组成的箭头:- 和 >
- 一个体,比如f.getName().endsWith(“.xml”)
lambda表达式有两种形式。当你的lambda表达式体中包含一个语句的时候可以采用第一种:
(parameters) -> expression
当你的lambda表达式体中包含一个或多个语句的时候可以使用第二种形。式请注意,你必须使用大括号将表达式包含进来:
(parameters) -> { statements;}
大体来说,如果lambda表达式参数的类型被间接的指出过,可以去掉类型声明。另外,如果参数个数只是一个,那么圆括号也可以被去掉。
Lambda表达式使用场景
现在,了解了如何写一个lambda表达式,接下来的问题是考虑使用lambda表达式的使用方式和使用场景。简单的说,你可以在函数接口中使用lambda表达式。函数接口里包含一个抽象方法。比如上文的两个lambda表达式。
Runnable r = () -> System.out.println("Hi");
FileFilter isXml = (File f) ->
f.getName().endsWith(“.xml”);
Runnable就是一个函数接口,因为它包含了一个抽象方法run。FileFilter也是一个函数接口,因为它也定义了一个抽象方法叫accept。
@FunctionalInterface
public interface Runnable {
void run();
}
@FunctionalInterface
public interface FileFilter {
boolean accept(File pathname);
lambda表达式重要的一点就是让你创建函数接口的实例。lambda表达式的体提供了函数接口中单个抽象方法的实现。因此,下面使用匿名类和lamba表达式实现的Runnable的结果是一直的。
Runnable r1 = new Runnable() {
public void run() {
System.out.println(“Hi!”);
}
};
r1.run();
Runnable r2 = () -> System.out.println(“Hi!”);
r2.run();
注意事项
你会经常看到在接口类上标有@FunctionalInterface的注解。它和标签@Override很相似,表示方法是可以被覆盖的。本文中,@FunctionalInterface标签用来写在文档中,表示接口是一个函数接口。编译器也将在接口注解不符合函数接口定义的时候抛错。
你也将发现一些新的函数接口,比如在包java.util.function里面的Function<T, R>和Supplier<T>,你可以通过这些来使用任意形式的lambda表达式。
方法参数
方法参数允许你重用已经存在的方法定义并将它们像lamda表达式一样传递进来。它们的实用性在你写代码的时候展现,会使代码跟单纯的使用lambda表达式比更自然更易读。比如使用lambda表达式找隐藏的文件。
File[] hiddenFiles = mainDirectory.listFiles(f -> f.isHid
den());
使用方法参数,你能够使用双冒号直接应用方法isHidden。
File[] hiddenFiles = mainDirectory.listFiles(File::isHidden);
最直接理解lambda表达式的方法参数的方式是叫它”指定方法”。
方法参数的类型有以下四种:
1、静态方法的方法参数
Function<String, Integer> converter = Integer::parseInt;
Integer number = converter.apply(“10”);
2、实例方法的方法参数。
意思是将一个对象的方法运用到lambda表达式的第一个参数位置上。
Function<Invoice, Integer> invoiceToId = Invoice::getId;
3、已有对象的实例化方法参数。
Consumer<Object> print = System.out::println;
需要指出的是,这种方法参数在你遇到一个私有辅助方法并想将其注入到另一个方法中时非常有用。
File[] hidden = mainDirectory.listFiles(this::isXML);
private boolean isXML(File f) {
return f.getName.endsWith(“.xml”);
}
4、构造器参数:
Supplier<List<String>> listOfString = List::new;
大汇合
在本章的开始,你看到了一段冗长的分类变量invoices的java代码。
Collections.sort(invoices, new Comparator<Invoice>() {
public int compare(Invoice inv1, Invoice inv2) {
return Double.compare(inv2.getAmount(), inv1.getAmount());
}
});
现在,你将看到如何通过我们目前所掌握的java8特性重构这段代码,使其变得更可读更简洁。
首先,Comparator是一个函数接口,它仅定义了一个抽象方法叫“compare”,这个类传入了两个相同类型的对象,并返回一个整数。这种情况lambda表达式能够有很好的表现,如:
Collections.sort(invoices,
(Invoice inv1, Invoice inv2) -> {
return Double.compare(inv2.getAmount(),
inv1.getAmount());
});
我们发现lambda表达式的体仅仅的返回一个简单的表达式,所以可以使用更加简洁的形式:
Collections.sort(invoices,
(Invoice inv1, Invoice inv2)
-> Double.compare(inv2.getAmount(),
inv1.getAmount()));
java8中List接口支持sort方法,所以可以使用List代替Collections.sort,如下:
invoices.sort((Invoice inv1, Invoice inv2)
-> Double.compare(inv2.getAmount(),
inv1.getAmount()));
接下来,java8引入了一个静态辅助方法Comparator.comparing,它使用一个lambda参数抽取出比较的key值。该地方最后生成了一个Compare对象。你可以采用如下方法使用它。
Comparator<Invoice> byAmount
= Comparator.comparing((Invoice inv) -> inv.getAmount());
invoices.sort(byAmount);
你可能注意到了,更简洁的办法是采用Invoice::getAmount代替(Invoice inv)
-> inv.getAmount()。如下:
Comparator<Invoice> byAmount
= Comparator.comparing(Invoice::getAmount);
invoices.sort(byAmount);
由于getAmount方法返回的是一个double类型的值,所以使用Comparator.comparingDouble指定类型能够避免一些不必要的问题:
Comparator<Invoice> byAmount
= Comparator.comparingDouble(Invoice::getAmount);
invoices.sort(byAmount);
最后,让我们整理下代码,使用import static引入方法,去掉持有Comparator对象的变量,给出本章开头引入的问题的解决方案。
import static java.util.Comparator.comparingDouble;
invoices.sort(comparingDouble(Invoice::getAmount));
使用Lambda表达式测试
你可能关心lambda表达式如何影响测试。毕竟,lambda表达式引入的行为需要被测试验证。当你决定测试包含lambda表达式的时候,可以考虑以下两个方面。
如果lambda表达式很小,那么可以去测试使用了lambda表达式的周围代码行为。
如果lambda表达式非常复杂,则抽取它们到一个单独的方法参数中,这样你就可以将其注入并独立测试它们了。
总结
本章的重点概念如下:
lambda表达式可以被理解为一种匿名函数。
Lambda表达式和行为参数模型会代码更灵活更简洁。
函数接口是一个仅声明了一个抽象方法的接口。
Lambda表达式只能用在函数接口上下文中。
在你需要重用一个已有方法时,方法参数比单纯的lambda表达式更加自然,只需要将该方法传入参数位置即可。
测试时,将复杂的lambda表达式拆开,方便你将它们注入到方法参数中。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/114504.html