可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式。
Lambda表达式包括三个部分:参数列表,箭头,Lambda主体。例如:(String s) -> s.length();
在哪里使用Lambda表达式
函数式接口:只定义了一个抽象方法(可以包括若干默认方法)的接口是函数式接口。
Lambda表达式允许直接以内联的方式为函数式接口的抽象方法提供实现,并把整个表示打作为函数式接口的实例(该实例即简化后的匿名函数的实例)
Runnalbe r1 = () -> System.out.println("这是Lbamda表示式");
Runnable r2 = new Runnable() {
public void run() {
System.out.println("这是匿名函数");
}
}
函数描述符和常用的函数式接口
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。这种抽象方法就叫做函数描述符。使用一种特殊的表示法来描述Lambda和函数式接口的签名。例如() -> void代表参数列表为空,且返回void的函数,即Runnable接口所代表的签名。
函数式接口 | 函数描述符 |
---|---|
Predicate |
T->boolean |
Consumer |
T->void |
Function |
T->R |
Supplier |
()->T |
UnaryOperator |
T->T |
BinaryOperator |
(T, T)->T |
BiPredicate |
(L, R)->boolean |
BiConsumer |
(T, U)->void |
BiFunction |
(T, U)->R |
类型检查
如何确定Lambda表达式的类型是从使用的上下文中确定的。
以List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
为例
1查看使用的上下文,filter函数定义,确定目标类型是PredicateList<Apple> filter(List<Apple> inventory, Predicate<Apple> p)
2确定接口的抽象方法,Predicateboolean test(Apple apple)
3确定函数描述符,Apple ->boolean
。
4比较函数描述符和Lambda表达式的签名。发现相匹配,类型检查正确
如果lambda表达式抛出异常,则抽象方法所声明的throws语句也必须与之匹配。
public interface ThrowLambda {
void test(String s) throws InvalidParameterException;
}
ThrowLambda lambda = (s) -> {
if (s == null) {
throw new InvalidParameterException();
}
};
我尝试了代码,感觉这个限制不是必须的
public interface ThrowLambda {
void test(String s);
}
ThrowLambda lambda = (s) -> {
if (s == null) {
throw new InvalidParameterException();
}
System.out.println("有效入参");
};
try {
lambda.test(null);
} catch (InvalidParameterException e) {
System.out.println("无效入参");
}
// 正常打印了 无效入参
针对类型检查的原理,只要能将函数描述符和Lambda表达式的签名匹配上,同一个Lambda表达式可以匹配不同的函数式接口。
public interface Supplier<T> {
T get();
}
public interface Temperature<Integer> {
Integer getTemperature();
}
Supplier<Integer> supplier = () -> 42;
Temperature<Integer> temperature = () -> 42;
Cpmparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareT0(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 =
(Apple a1, Apple a2) -> a1.getWeight().compareT0(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 =
(Apple a1, Apple a2) -> a1.getWeight().compareT0(a2.getWeight());
方法引用
方法引用可以看错是一种调用特定的Lambda表达式的一种快捷写法。方法引用就是根据已有的方法实现来创建Lambda表达式。但是显式地指明方法的名称,代码的可读性会更好。
使用方法就是将目标引用放在分隔符 :: 前,方法的名称放在分隔符后。例如Apple::getWeight
就是引用了Apple类中定义的getWeight方法
三种方法引用方式
1指向静态方法的方法引用。例如Integer::parseInt
2指向任意类型实例方法的方法引用。例如:String::getLength
3指向现有对象的实例方法的方法引用。有一个局部变量salary,属于Salary类,有一个add的方法,则可以这么调用salary::add
那么2和3有什么区别呢?
当对象本身就是Lambda表达式的一个参数时,我们使用第二种方式。例如:
Function<String, Integer> f1 = String::length;
Function<String, Integer> f2 = (string) -> string.length();
当只是要在Lambda表示式中调用一个已经存在的外部对象的方法时,使用第三方重视。例如:
public class Salary {
private int a = 0;
public int add(int number) {
a = a + number;
return a;
}
}
Salary salary = new Salary();
Function<Integer, Integer> f1 = salary::add;
Function<Integer, Integer> f2 = (addNumber) -> salary.add(addNumber);
闭包和Lambda表达式局部变量限制
闭包就是一个函数的实例,且它可以无限制的访问函数的非本地变量。
Lambda和匿名类可以做到类似的事情:可以作为参数传递给方法,且可以访问其作用域之外的变量。但是java对其做出了限制,只能捕获指派给它们的局部变量一次(而实例变量和静态变量则没有限制)。这就意味着在Lambda和匿名类中不能修改外部传入的局部变量。
为什么要限制
因为局部变量是保存在栈上,它们应该仅限于它们本身所在的线程。而Lambda表达式可能被运行于其它线程。如果允许Lambda表达式修改这些局部变量,就会引发造成线程不安全的新的可能性,更加难以维护。而堆是线程之间共享的,所以实例变量和静态变量可以被修改(注意,这里实例变量可以被修改是指的实例变量对象,实例变量的引用也是局部变量,不能被修改,即不能让实例变量的引用指向另外一个实例变量)。