Lambda 表达式其实蕴含的是函数式编程中一个非常重要的思想:行为参数化。行为参数化就是一个方法接受不同的行为作为参数,以实现不同的功能。行为参数化可以使代码变得简洁,更重要的是它可以让代码适应不断变化的要求。
在 Java 8 之前,实现行为参数化的最佳做法是采用策略模式,但 Lambda 表达式更加容易实现。
一、函数式接口
函数式接口是只有一个抽象方法的接口。
在 Java 8 中,允许接口拥有默认方法,也就是在没有实现类来实现这个方法时,接口也提供了一个默认实现。默认方法要用关键字 default
修饰。但是不管有多少个默认方法,只要接口只定义了一个抽象方法,那么这个接口就是函数式接口。
Lambda 表达式可以直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。Lambda 表达式本质上就是一个匿名函数。
二、Lambda 表达式的基本用法
Lambda 表达式的一般形式
Lambda 表达式可写成这两种形式:(parameters) -> expression
或 (parameters) -> {statements;}
。此处 expression 和 statements 之间的区别值得一提:
- expression 是指表达式,表达式就是包含变量和运算符(也可以没有运算符)的一个式子,比如说
a
和a+b
都是表达式。 - statement 是指语句,语句就是由分号结尾的,比如
int i = 0;
和retrun "a";
都是语句。表达式加上分号之后也可以成为语句。花括号内可以写多条语句。
如何使用 Lambda 表达式
Java 8 之前,如果要实现一个函数式接口,比如很常见的 Runnable 接口,第一种做法是写一个实现类:
//Runnable接口的实现类
class RunnableImpl implements Runnable
{
@Override
public void run() {
System.out.println("Hello,world!");
}
}
public class NoLambdaTest {
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
Thread thread = new Thread(runnable);
thread.start();
}
}
但是这种做法比较费事,所以更常见的做法是把 Runnable 接口的实现写成匿名内部类:
public class NoLambdaTest {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("Hello,world!");
}
});
thread.start();
}
}
但不管是哪种写法,都仅仅是为了实现一个方法,为此而写出一个类是很不划算的;Lambda 表达式就很好地解决了这个问题:
public class LambdaTest {
public static void main(String[] args) {
//用Lambda表达式来实现接口
Runnable runnable = () -> System.out.println("Hello,world!");
Thread thread = new Thread(runnable);
thread.start();
}
}
三、Lambda 表达式的类型
基本概念
前面说过 Labmda 表达式实际上是一个匿名函数,既然是函数就有参数类型和返回值类型。
Lambda 表达式的参数类型与返回值类型要和接口中抽象方法所声明类型相同。比如 Runnable 中的 run 方法没有参数、没有返回值,那么对应的Lambda表达式也就没有参数、没有返回值;Predicate<T> 接口中的 test 方法接受一个 T 类型的对象,返回一个 boolean 值,那么 Lambda 表达式的参数就是一个 T 类型的对象,返回值是 boolean 值。
通常使用 (paramters) -> returns
这样一个式子来描述 Lambda 表达式的类型,比如上面提到的 test 方法,实现它的 Lambda 表达式的类型就是 T -> boolean
。
同一个 Lambda 表达式可以应用于不同的函数式接口,只要类型是对应的就行。
类型推断
编译器会根据上下文推断出应该用什么函数式接口来配合 Lambda 表达式,所以可以无需显式地指出参数的类型。
Predicate<Integer> atLeast5 = x -> x > 5;
编译器可以进行类型推断,因此无需指明 x
的类型。但要注意,推断的前提是已经指定这个泛型接口的具体类型 ,因此 Predicate<Integer> 这里的类型是不可省略的。
类型检查
为了保证 Lambda 表达式和其所实现的函数式接口的类型是相同或者兼容的,编译器需要进行类型检查。下图展示了编译器对 Predicate<Integer> atLeast5 = x -> x > 5;
进行类型检查的过程。
四、使用技巧
使用局部变量
Lambda 表达式可以引用定义在 Lambda 表达式主体之外的局部变量,但是有一个限制:这个局部变量要么声明为 final
,要么是事实上的 final
——仅赋值一次。
public class LambdaTest {
public static void main(String[] args) {
int sum = 3;
//引用sum
Runnable runnable = () -> System.out.println("1 + 2 = " + sum);
Thread thread = new Thread(runnable);
thread.start();
}
}
尽管 sum
没有用 final
修饰,但 sum
是一个事实上的 final
变量,因此引用是合法的。
避免装箱
为了避免装箱操作以提高效率, Predicate<T> 等通用式函数式接口提供了针对原生数据类型的特化形式 IntPredicate,LongPredicate,DoublePredicate 等。
方法引用
如果 Lambda 表达式的主体只是对一个已有方法的调用,那么就可以用方法引用来进一步简化 Lambda 表达式。方法引用的格式为 ClassName :: Method
。
注意,方法引用所引用的方法的签名必须与上下文类型匹配。
构建方法引用
1.引用静态方法:比如引用 Integer.parseInt
,可以写为 Integer::parseInt
。
2.引用实例方法:比如引用 String 的 length
方法,可以写为 String::length
。
3.引用已有对象的实例方法:比如有一个局部变量 val ,要引用它的 toString
方法,可以写为 val::toString
。
引用构造器
不管构造器的签名如何,通过方法引用来引用构造器的形式都是 ClassName::new
。参数个数不同的构造器适用于不同的函数式接口:
- 无参构造器:适合 Supplier<T> 接口
- 有一个参数的构造器:适合 Function<T,R> 接口
- 有两个参数的构造器:适合 BiFunction<T,R> 接口
参考文献
《Java 8实战》Raoul-Gabriel Urma,Mario Fusco,Alan Mycroft 著
《Java 8函数式编程》Richard Warburton 著