初识Lambda表达式
上一篇中,我们介绍了行为参数化的编程模式,它能够轻松地适应不断变化的需求。在介绍这一模式时,我们提到了Lambda表达式,它使我们在封装行为的时候代码更简洁,可读性更强。在这一篇中,我们将详细介绍Lambda表达式的细节,和更多的用法。首先让我们来看几个例子。
例1 Comparator来排序
对集合排序是一个常见的编程任务,例如上一篇中的苹果集合,我们想对它们按照重量或者颜色排序,按照行为参数化的模式,我们应该用一种方法来表示和使用不同的排序行为。而在Java的List中自带了一个sort方法(Collections.sort)sort的行为可以用java.util.Comparator对象来参数化,接口如下:
//java.util.Comparator
public interface Comparator{
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
@FunctionalInterface
public interface Predicate{
boolean test(T t);
}
public static List filter(List lst, Predicate p){
List results = new ArrayList<>();
for(T t : lst){
if(p.test(s)){
results.add(s);
}
}
}
Predicate nonEmpty = (String s) -> !s.isEmpty();
List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer
@FunctionalInterface
public interface Consumer{
void accept(T t);
}
public static void forEach(List list,Consumer 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
@FunctionalInterface
public interface Function{
R apply(T t);
}
public static List map(List list,Fuunctionf){
List result = new ArrayList<>();
for(T s:list){
result.add(f.apply(s));
}
return result;
}
List list = map(Arrays.asList("lambdas" , "in", "java"),(String s) -> s.length());
原始类型特化
Java中的类型要么是引用类型(比如Bye、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。我们上面介绍的3个范型函数式接口:Predicate
我们都知道,Java中将原始类型转换为对应的引用类型的机制,叫做装箱(boxing);而同样会有一个相反的将引用类型转换为对应原始类型的机制,叫做拆箱(unboxing);装箱和拆箱在Java中是由自动装箱机制来自动完成的,这样的机制减少了很多编码的工作,但是在性能方面会付出代价。为了让我们的函数式接口在输入和输出都是原始类型值的时候避免不必要的自动装箱操作,Java为我们的函数式接口提供了针对特定类型的特化版本。
这里我们以Predicate
//这里会把参数1000装箱到一个Integer对象中。
Predicate 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