Lambda表达式

定义

lambda表达式,是用来实现行为参数化的一个载体,或者称之为工具,它可以很简洁地表示一个行为或者传递代码。这与java中的匿名函数一样。

对于lambda表达式的官方定义是这么说的:

可以把lambda表达式理解为简洁地表示可传递匿名函数的一种方式:它没有名称,但是有参数列表,函数主体,返回类型,可能还有一个可以抛出的异常列表。这个定义比较广泛,一步步拆分开来解释。

  • 匿名:它不像普通方法一样,有一个非常明确的名称,写得少而想得多
  • 函数:说lambda表达式是一个函数,而不是一个类,是因为lambda函数不像方法那样属于某个特定的类,但是和方法一样,有参数列表,函数主体,返回类型,还可能有可以抛出的异常列表。
  • 传递:Lambda表达式可以作为参数传递给方法或者存储在变量中
  • 简洁:无需像匿名类一样写很多模板代码

而lambda表达式由三部分组成:参数、箭头、主体

延伸

函数式接口

定义:只定义了一个抽象方法的接口,但该接口允许有多个默认方法。

1
2
3
4
5
6
7
8
例如:
public interface Comparator<T> {
int compare(T o1, T o2);
}
以及Runnable接口:
public interface Runnable{
void run();
}

作用:lambda表达式允许你直接以内联的形式为函数式接口的抽象放阿飞提供实现,并且把整个表达式作为函数式接口的实例(具体来来说,是函数式接口一个具体实现的实例)。

以Runnable接口为例,下面以lambda表达式形式展现的代码是有效的:

1
2
3
4
5
6
7
Runnable r1 = () -> System.out.println("Hello Java8");
public static void process(Runnable runnable){
runnable.run();
}
process(r1);
或者直接
process(() -> System.out.println("Nice to Meet you!"));

函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名,我们称此种抽象方法为函数描述符。例如Runnable接口可以看做一个什么也不接受什么也不返回的函数的签名,因为它只有一个run抽象方法,这个方法什么也不接受,什么也不返回。

所以,Runnable接口的函数描述符就是() -> void,它代表参数列表为空,且返回类型为void的函数。

JavaAPI

Java API中其实已经有了几个函数式接口,以Java8为例,在java.util.function包中,引入了几个新的函数式接口。这里主要介绍三种函数式接口:Predicate、Consumer、Function。这三个接口是在Java8中提供的Stream API中应用最为广泛的接口。

1) Predicate

Predicate<T>接口中定义了一个名为test的抽象方法,表示接受一个泛型对象T并返回一个Boolean(函数描述符:(T t) -> Boolean)。下面就是Predicate接口的一个简单示例。

1
2
3
4
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
1
2
3
4
5
6
7
8
9
10
11
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();
for (T t : list){
if (p.test(t)){
result.add(t);
}
}
return result;
}
Predicate<String> nonEmpty = (String s) -> !s.isEmpty();
List<String> nonEmpty = (List<String>) filter(listOfString, nonEmpty);

2) Consumer

Consumer<T>接口则定义了一个名为accept的抽象方法,它接受一个泛型T的对象,并没有任何返回(函数描述符:(T t) -> void) 即如果需要访问类型T的对象,并对其执行相应的操作,便可使用该接口。Stream API中的forEach方法,就是利用该接口进行实现的。

1
2
3
4
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}

下面就是利用Consumer实现一个foreach的小例子:

1
2
3
4
5
6
7
public static <T> void forEach(List<T> list, Consumer<T> c) {
for (T t :list) {
c.accept(t);
}
}
List<String> listOfString = new ArrayList<>();
listOfString.forEach(s -> System.out.println(s));

3) Function

Function<T, R>接口定义了一个叫做apply的抽象方法,表示它接受一个泛型T的对象,并返回泛型R的对象(函数描述符:(T t) -> R)即Stream API中的map方法就是对Function的一个典型应用的例子。

1
2
3
4
5
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
List<Integer> lengthOfString = map(Arrays.asList("lambda","in","action"),s -> s.length());

类型检查,推断和限制

Lambda表达式中的参数部分,可以省略类型,及表达式可以简化为(a1, a2) -> {……}。那么问题来了,Lambda表达式时如何推断参数类型的?再者,Lambda表达式本身并不包含它在实现哪个函数式接口的信息。为了全面了解Lambda表达式,很有必要了解Lambda本身的实际类型是什么。

类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受他传递方法的参数,或者接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。具体的推断过程如下图所示。

方法引用

方法引用,可以被看做是仅仅调用特定方法的Lambda表达式的一种快捷写法。换句话说,如果lambda表达式是直接调用一个类中现成的方法时,可以用方法引用代替。

举个例子:

当需要比较促销商品中的促销价格,不使用方法引用,写法如下:

1
Comparator<PromotionGoods> c1 = (g1, g2) -> g1.getPromotionSalePrice().compareTo(g2.getPromotionSalePrice());

观察上述lambda表达式可知,lambda表达式中,只是直接调用了PromotionGoodsgetPromotionSalePrice()方法去获取促销商品价格,进而调用compareTo方法进行比较,而没有其他多余的业务逻辑操作。

此时,就可以用方法引用来代替:

1
Comparator<PromotionGoods> c1 = Comparator.comparing(PromotionGoods::getPromotionSalePrice);

使用方法引用之后,可以发现,代码具有更好的可读性。

如何使用

当需要使用方法引用时,目标引用和对应的方法名称,用::进行分隔。就像上一节的代码 :PromotionGoods::getPromotionSalePrice,其实本质就是Lambda表达式:(PromotionGoods goods) -> goods.getPromotionSalePrice()的快捷写法。这里需要注意,方法名称,并不需要带上(),因为你并没有实际调用这个方法。