Lambda表达式
先暂时将Java8中的Lambda表达式看成匿名的函数,它跟一般的方法类似,一样具有参数类型、函数主体、返回值甚至可抛出异常列表。但是,它与一般方法最大的不同是:它可以作为参数传递给方法或者存储在变量中。可赋值给变量,这是Lambda表达式在Java中应用的基础。
Lambda表达式的语法结构如下:
(parameters) -> expressions
或
(parameters) -> {statements}
需要注意的是,控制流程语句必须在{}中,return语句也属于控制流程语句。
Lambda表达式一定要赋予特定的变量才可以使用,接收其值的变量是一个函数式接口的实例。也就是说,整个Lambda表达式为函数式接口的抽象方法提供了实现,它是函数式接口的一个具体实现的实例。
函数式接口
在Java8中,有且只有一个抽象方法的接口就是一个函数式接口。新版本的Java中接口可以拥有默认方法,函数式接口可以拥有多个默认方法,但必须只能存在一个抽象方法。
为确保接口一定是函数式接口,可以在接口上使用 @FunctionalInterface 注解。加上注解,如果接口不满足函数式接口的要求,将在编译时报错。
既然Lambda表达式视为函数式接口的实现实例,它们存在一定的对应关系,比如:
Runnable v1 = () -> System.out.println("Hello");
//等价于
Runnable v2 = new Runnable() {
public void run() {
System.out.println("Hello");
}
};
函数描述符
在将Lambda表达式赋值给函数式接口变量的时候,编译器根据上下文做类型检查,检查的依据是函数描述符。这里函数描述符主要指表达式的参数列表和返回值,即在应用Lambda表达式时需要确保表达式的描述与函数式接口的定义的目标类型保持一致。同一个表达式,在不同的上下文中有可能对应不同的函数式接口。
特殊的void兼容:如果Lambda表达式的主体是一个语句表达式,那么它就和一个返回void的函数描述符兼容,示例如下:
//Predicate接口的抽象方法:boolean test(T t);
Predicate<String> p1 = (String s) -> s.contains("A");
//Consumer接口的抽象方法:void accept(T t);
//这里这个例子没有使用价值,只是说明void的特殊性
Consumer<String> c1 = (String s) -> s.contains("A");
Lambda表达式的类型推断
Java编辑器可以根据上下文中的目标类型推断函数描述符,通过这个特性,可以省略参数类型,还是上面的例子:
Predicate<String> p1 = s -> s.contains("A");
Consumer<List<Integer>> c1 = l -> l.add(100);
至此,根据Lambda表达式与函数式接口的对应关系,可以自定义函数式接口并使用表达式,使得传递不同行为的实现既方便又简洁,小示例:
public class Test {
public static void main(String[] args) {
func(1, 2, (a, b) -> a + b);
func(2, 2, (a, b) -> a * b);
func(9, 18, (a, b) -> a * 10 + b % 10);
}
static void func(int a, int b, IntOperation opt) {
System.out.println(String.format(String.format("a: %d, b: %d", a, b)));
System.out.println(String.format("result: %d", opt.operate(a, b)));
}
}
interface IntOperation {
int operate(int a, int b);
}
非受检异常限制
任何函数式接口都不允许抛出受检异常。
这里应该指的是Java标准API提供的函数式接口,我们可以通过以下两个方式处理受检异常。
- 自己定义函数式接口并声明可抛出指定的受检异常。
- 在Lambda表达式中使用try/catch捕获受检异常
Lambda表达式访问外部变量的限制
1.Lambda表达式可以自由的访问实例变量和静态变量,没有任何限制,如下:
static int num = 100;
public static void main(String[] args) {
func(2, 2, (a, b) -> a + b + num);
num = 10;
}
2.Lambda表达式访问的局部变量必须是final的或者隐式final的
public static void main(String[] args) {
int num = 100;
func(2, 2, (a, b) -> a + b + num);
//这里如果对num进行修改将在编译时报错
//num = 10;
}
对于局部变量限制存在的解释是:局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性。实例变量可以自由访问的原因是,它们保存在堆中,而堆是线程共享的;静态变量可以自由访问的原因是,静态变量这样的类数据保存在方法区中,方法区同样是线程共享的。
方法引用
方法应用可以看做Lamdba表达式在特定情况下的更加易读的简便写法。
方法引用主要有三类:
1. 指向静态方法的方法引用,像Integer::parseInt,与lambda表达式的对应结构:
(args) -> ClassName.staticMethod(args)
//等价
ClassName::staticMethod
2.指向任意类型实例方法的方法引用,像String::length,对应结构:
(arg0,rest) -> arg0.instanceMethod(rest)
//等价
ClassName::instanceMethod
3.指向现有对象的实例方法的方法引用,像oneNum::intValue,对应结构:
(args) -> expr.instanceMethod(args)
//等价
expr::instanceMethod
在使用方法引用的时候,也是需要注意他可以解包的Lambda表达式的函数描述符,只要描述符一致,就可以被函数式接口变量接收。
注:引用等摘自《Java8实战》