JAVA函数式编程(2) - Lambda表达式与函数式接口
初识Lambda表达式
上一篇中,我们介绍了行为参数化的编程模式,它能够轻松地适应不断变化的需求。在介绍这一模式时,我们提到了Lambda表达式,它使我们在封装行为的时候代码更简洁,可读性更强。在这一篇中,我们将详细介绍Lambda表达式的细节,和更多的用法。首先让我们来看几个例子。
例1 Comparator来排序
对集合排序是一个常见的编程任务,例如上一篇中的苹果集合,我们想对它们按照重量或者颜色排序,按照行为参数化的模式,我们应该用一种方法来表示和使用不同的排序行为。而在Java的List中自带了一个sort方法(Collections.sort)sort的行为可以用java.util.Comparator对象来参数化,接口如下:
//java.util.Comparator
public interface Comparator<T>{
public int compare(T o1, T o2)
}
此时我们可以使用Lambda函数来实现这一接口,按照重量对苹果序列进行升序排序
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
例2 用Runnable执行代码块
在进行并发编程的时候,我们常常需要在线程中执行一个代码块,我们怎么才能告诉我们的线程要执行哪个代码块呢?Java提供了一个Runnable 接口专门用来表示一个要执行的代码块,接口如下:
//java.lang.Runnable
public interface Runnable{
public void run();
}
我们同样可以用Lambda表达式来实现这样一个接口。
//Runnable实现
Thread t = new Thread(() -> System.out.println("Hello world"));
例3 GUI事件处理
我们在进行GUI编程的时候常常需要执行一个操作来响应特定的事件,如鼠标单击或在文字上悬停等等。而响应事件的操作可以定义为EventHandler接口:
//EventHandler
public interface EventHandler{
public void handle(ActionEvent event);
}
如果我们要使其用于响应鼠标单击按钮事件,然后改变文本框的文字,我们可以用如下Lambda表达式来实现:
//EventHandler 实现
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));
函数式接口
可以发现上述三个接口类都有一个共同的特征:那就是只定义一个抽象方法。我们称这样的接口类是一个函数式接口。Lambda表达式允许你直接以内联的形式为函数式接口提供实现,并把整个表达式作为函数式接口的实例,甚至可以赋值给一个函数式接口实例。
//Lambda函数对函数式接口示例化
Runnable r1 = () -> System.out.println("Hello word!");
函数式接口定义且只定义了一个抽象方法,我们可以用这个仅有的抽象方法的签名来描述描述Lambda表达式。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口,这里我们介绍几个java.util.function包中的几个函数式接口。
Predicate
java.util.function.Predicate<T> 接口定义了一个叫做test的抽象方法,它接受范型T对象,并返回一个boolean。这恰恰和你先前创建的一样,现在就可以直接使用了。在你需要表示一个涉及类型T的布尔表达式,就可以使用这个接口。比如,你可以定义一个接受string对象的Lambda表达式,如下所示:
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static<T> List<T> filter(List<T> lst, Predicate<T> p){
List<T> results = new ArrayList<>();
for(T t : lst){
if(p.test(s)){
results.add(s);
}
}
}
Predicate<String> nonEmpty = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受范型T的对象,没有返回值。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如我们经常对collection对象用到的forEach方法。
@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);
}
}
forEach(Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i));
Function
java.util.function.Function<T, R> 接口定义了一个叫做apply的方法,它接受一个范型T的对象,并返回一个范型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口。在下面的代码中,将展示如何利用Function接口来涉及一个map方法,将一个String列表映射到包含每个String长度的Integer列表。
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list,Fuunction<T, R>f){
List<R> result = new ArrayList<>();
for(T s:list){
result.add(f.apply(s));
}
return result;
}
List<Integer> list = map(Arrays.asList("lambdas" , "in", "java"),(String s) -> s.length());
原始类型特化
Java中的类型要么是引用类型(比如Bye、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。我们上面介绍的3个范型函数式接口:Predicate<T>、Consumer<T>和Function<T,R>只能绑定到引用类型,这是由范型的内部实现方式造成的。
我们都知道,Java中将原始类型转换为对应的引用类型的机制,叫做装箱(boxing);而同样会有一个相反的将引用类型转换为对应原始类型的机制,叫做拆箱(unboxing);装箱和拆箱在Java中是由自动装箱机制来自动完成的,这样的机制减少了很多编码的工作,但是在性能方面会付出代价。为了让我们的函数式接口在输入和输出都是原始类型值的时候避免不必要的自动装箱操作,Java为我们的函数式接口提供了针对特定类型的特化版本。
这里我们以Predicate<T>为例,首先是范型版本:
//这里会把参数1000装箱到一个Integer对象中。
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);
让我们再来看特化版本:
//这里1000将会直接作为一个int类型变量被使用。
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);
一般来说针对专门的输入参数类型的函数是接口名称都要加上对应的原始类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。(Function接口还有针对输出参数类型的变种:ToIntFunction<T>、IntToDoubleFunction等。)