通过一个例子,看看在实践中如何利用Lambda
和行为参数化来让代码更为灵活,更为简洁。资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。这就是所谓的环绕执行(execute around)模式,如下图所示。例如,在以下代码中,中间部分就是从一个文件中读取一行所需的模板代码(注意使用了Java 7中的带资源的try语句,它已经简化了代码,因为不需要显式地关闭资源了):
public static String processFile() throws IOException {
//这就是做有用工作的那行代码
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
}
}
现在这段代码是有局限的。只能读文件的第一行。如果想要返回头两行,甚至是返回使用最频繁的词,该怎么办呢?在理想的情况下,要重用执行设置和清理的代码,并告诉processFile
方法对文件执行不同的操作。这听起来是不是很耳熟?是的,需要把processFile
的行为参数化。需要一种方法把行为传递给processFile
,以便它可以利用BufferedReader
执行不同的行为。
传递行为正是Lambda
的拿手好戏。那要是想一次读两行,这个新的processFile
方法看起来又该是什么样的呢?基本上,需要一个接收BufferedReader
并返回String
的Lambda
。例如,下面就是从BufferedReader
中打印两行的写法:
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
前面解释过了,Lambda
仅可用于上下文是函数式接口的情况。需要创建一个能匹配BufferedReader -> String
,还可以抛出IOException
异常的接口。把这一接口叫作BufferedReaderProcessor
吧。
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
现在就可以把这个接口作为新的processFile
方法的参数了:
public static String processFile(BufferedReaderProcessor p) throws IOException {
}
任何BufferedReader -> String
形式的Lambda
都可以作为参数来传递,因为它们符合BufferedReaderProcessor
接口中定义的process
方法的签名。现在只需要一种方法在processFile
主体内执行Lambda
所代表的代码。请记住,Lambda
表达式允许直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。因此,可以在processFile
主体内,对得到的BufferedReaderProcessor
对象调用process
方法执行处理:
public static String processFile(BufferedReaderProcessor p) throws
IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
//处理BufferedReader
return p.process(br);
}
}
现在你就可以通过传递不同的Lambda重用processFile方法,并以不同的方式处理文件了。处理一行:
String oneLine = processFile((BufferedReader br)-> br.readLine());
处理两行:
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
下图总结了所采取的使pocessFile方法更灵活的四个步骤:
函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda
表达式的签名。函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda
表达式,需要一套能够描述常见函数描述符的函数式接口。Java API
中已经有了几个函数式接口,比如Comparable
、Runnable
和Callable
。
Java 8
的库设计师在java.util.function
包中引入了几个新的函数式接口:Predicate
、Consumer
和Function
。
java.util.function.Predicate
接口定义了一个名叫test
的抽象方法,它接受泛型T
对象,并返回一个boolean
。这恰恰和先前创建的一样,现在就可以直接使用了。在需要表示一个涉及类型T
的布尔表达式时,就可以使用这个接口。比如,可以定义一个接受String
对象的Lambda
表达式,如下所示:
@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;
}
Predicatec<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listofStrings, nonEmptyStringPredicate);
如果去查Predicate接口的Javadoc
说明,可能会注意到诸如and
和or
等其他方法。现在不用太计较这些。
java.util.function.Consumer
定义了一个名叫accept
的抽象方法,它接受泛型T
的对象,没有返回(void
)。如果需要访问类型T
的对象,并对其执行某些操作,就可以使用这个接口。比如,可以用它来创建一个forEach
方法,接受一个Integers
的列表,并对其中每个元素执行操作。在下面的代码中,就可以使用这个forEach
方法,并配合Lambda
来打印列表中的所有元素:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c) {
for(T i : list){
c.accept(i);
}
}
//Lambda是Consumer中accept方法的实现
forEach(
Arrays.asList(1,2,3,4,5),
(Integer i) -> System.out.println(i)
);
java.util.function.Function
接口定义了一个叫作apply
的方法,它接受一个泛型T
的对象,并返回一个泛型R
的对象。如果需要定义一个Lambda
,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。在下面的代码中,将展示如何利用它来创建一个map
方法,以将一个String
列表映射到包含每个String
长度的Integer
列表。
@FunctionalInterface
public interface Punction<T, R> {
R apply(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;
}
//[7,2,6]
//Lambda是Punction接口的apply方法的实现
List<Integer> 1 = map(
Arrays.asList("lambdas","in","action"),
(String s)-> s.length()
);
三个泛型函数式接口:Predicate
、Consumer
和Function
。还有些函数式接口专为某些类型而设计。
Java
类型要么是引用类型(比如Byte
、Integer
、Object
、List
),要么是原始类型(比如int
、double
、byte
、char
)。但是泛型(比如Consumer
中的T
)只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此,在Java
里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱(unboxing)。Java
还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。比如,这就是为什么下面的代码是有效的(一个int
被装箱成为Integer
):
List<Integer> list = new ArrayList<>();
for(int i = 300; i < 400; i++) {
list.add(i);
}
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8
为前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,在下面的代码中,使用IntPredicate
就避免了对值1000
进行装箱操作,但要是用Predicate
就会把参数1000
装箱到一个Integer
对象中:
public interface IntPredicate {
boolean test(int t);
}
//true(无装箱)
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);
//false(装箱)
Predicate<Integer> oddNumbers =(Integer i) -> i % 2 == 1;
oddNumbers.test(1000);
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate
、IntConsumer
、LongBinaryoperator
、IntFunction
等。Function
接口还有针对输出参数类型的变种:ToIntFunction
、IntToDoubleFunction
等。
下表总结了Java API
中提供的最常用的函数式接口及其函数描述符。请记得这只是一个起点。如果有需要,可以自己设计一个。请记住,(T, U) -> R
的表达方式展示了应当如何思考一个函数描述符。表的左侧代表了参数类型。这里它代表一个函数,具有两个参数,分别为泛型T
和U
,返回类型为R
。
测验:函数式接口
对于下列函数描述符(即Lambda表达式的签名),请构造一个可以利用这些函数式接口的有效Lambda表达式:
(1)T -> R
(2)(int, int) -> int
(3)T -> void
(4)() -> T
(5)(T, U) -> R
答案如下。
(1)Function<T,R>不错。它一般用于将类型T的对象转换为类型R的对象(比如Function<Apple, Integer>用来提取苹果的重量)。
(2)IntBinaryOperator具有唯一一个抽象方法,叫作applyAsInt,它代表的函数描述符是(int, int)-> int。
(3) Consumer<T>具有唯一一个抽象方法叫作accept,代表的函数描述符是T -> void。
(4)Supplier<T>具有唯一一个抽象方法叫作get,代表的函数描述符是() -> T。或者,Callable<T>具有唯一一个抽象方法叫作call,代表的函数描述符是() -> T。
(5)BiFunction<T, U, R>具有唯一一个抽象方法叫作apply,代表的函数描述符是(T, U) -> R。
下表总结了一些使用案例、Lambda
的例子,以及可以使用的函数式接口:
异常、Lambda,还有函数式接口又是怎么回事呢?
请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,
并声明受检异常,或者把Lambda包在一个try/catch块中。
比如,函数式接口BufferedReaderProcessor,它显式声明了一个IOException:
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p =(BufferedReader br)-> br.readLine();
但可能是在使用一个接受函数式接口的API,比如Function<T, R>,没有办法自己创建一个。这种情况下,可以显式捕捉受检异常:
Function<BufferedReader, String> f = (BufferedReader b) -> {
try {
return b.readLine();
}
catch(IOException e) {
throw new RuntimeException(e);
}
};