Java中的Lambda表达式

Lambda表达式是Java8推出的新特性,虽然早有耳闻,但是平时从来没有用过,所以一直没有学习。来实习后才发现已经被大量使用,而且用起来确实非常方便,尤其是在大数据爆发的背景下,数据处理越来越频繁,而Lambda表达式恰好可以结合Stream类更简洁方便的处理数据流,因此在Java9出来之前,正好学习一下Lambda表达式。

一、Lambda表达式

1、特点

Lambda表达式可以看错是可以传递的匿名函数的一种方式:

  1. 匿名 - 没有名称
  2. 函数 - 有函数的一切组成部分
    • 参数列表
    • 函数主体
    • 返回类型
    • 可能有一个异常列表
  3. 传递 - 可以作为参数传递给方法或存储在变量中
  4. 简洁 - 无需像匿名类那样写很多模版代码

2、组成语法

Lambda表达式经常和匿名类一起提起,下面来一下他们两个的写法与区别。匿名类最常用的就是Comparator接口了。

1
2
3
4
5
6
7
8
9
10
   //匿名类
Comparator<Apple> byWeight = new Comparator<Apple>(){
@Override
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());//注意返回weight要为Integer才能调用compareTo
}
};

//Lambda表达式
byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

明显后者更加简洁。可以看出Lambda表达式有三个部分:

  • 参数列表:采用了Comparator中compare方法的参数,两个Apple。
  • 箭头:把参数列表与Lamda主体分隔开。
  • Lambda主体:比较两个Apple的重量。表达式就是Lambda的返回值。

Lambda的基本语法是:

1
2
3
(paramaters) -> expression
或者
(paramaters) -> {expression; }

二、Lambda表达式的使用方法

Lambda表达式的使用主要是在函数式接口上。

1、函数式接口

定义

函数式接口就是只定义一个抽象方法的接口

Java的API中也有一些函数式接口,比如:

1
2
3
4
5
6
7
8
9
10
11
public interface Comparator<T> {//java.util.Comparator
int compare(T o1, T o2);
}

public interface Runnable{//java.lang.Runnable
void run();
}

public interface Callable<V> {//java.util.concurrent.Callable
V call();
}

Java中接口现在可以有默认的实现了。哪怕有很多默认方法,只要是定义了一个抽象方法,就是函数式接口。

应用

Lambda表达式可以以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。也就是以更简单的方式实现匿名类的功能。

比如:

1
Runnable r1 = () -> System.out.println("hello");

2、函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫做函数描述符。主要看它的接收参数与返回类型。

3、FunctionalInterface声明

在Java的API中,函数式接口都带有@FunctionalInterface的标注。

这个标注的意义就是说明这个接口是一个函数式接口。

Java8中在java.util.function包中,自带了很多带有@FunctionalInterface标注的函数式接口。

Predicate

1
2
3
4
5
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
//下面还有很多default方法,省略
}

接收泛型T对象,返回一个Boolean。

Consumer

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

接收泛型T对象,没有返回(void)。可以用于对某个对象执行某些操作。

Function

1
2
3
4
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}

接收一个泛型T对象,返回一个泛型R的对象。可以用于将输入对象的信息映射到输出中。

Supplier

1
2
3
4
@FunctionalInterface
public interface Supplier<T> {
T get();
}

没有接受值,返回一个泛型T的对象。

基本类型

上面的都是泛型,如果输入基本类型,Java会自动进行装箱和拆箱操作,这样会浪费很多资源,因为装箱后会需要更多的内存。

因此针对专门的输入参数类型的函数式及饿哦库的名称都可加上对应的原始类型前缀。比如:

IntPredicate, DoublePredicate, IntFunction等等。

三、其他

1、类型检查

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

2、类型推断

Java编译器会从上下文(目标类型)推断出用上面函数式接口来配合Lambda表达式,这意味着它也可以推断出适合的Lambda签名,因为函数描述符可以通过目标类型来得到。

1
2
Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
Comparator<Apple> c1 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

这样代码可以进一步简化。

3、方法引用

方法引用可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们更易读。

使用方法

目标引用放在分隔符:: 前,方法的名称放在后面。例如:

Lambda 等效的方法引用
(Apple a) -> a.getWeight() Apple::getWeight
() -> Thread.currentThread().dumpStack() Thread.currentThread::dumpStack
(str, i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) System.out::println

可以把方法引用看做针对仅仅涉及单一方法的Lambda语法糖。

方法引用的构建

方法引用主要有三类:

  1. 指向静态方法的方法引用。

    例如Integer的parseInt方法,可以写作:Integer::parseInt。

  2. 指向任意类型实例方法的方法引用。

    例如String的length方法,写作String::length

  3. 指向现有对象的实例方法的方法引用。

Lambda 方法引用
(args) -> ClassName.staticmethod(args) ClassName::staticMethod
(arg0, rest) -> arg0.instanceMethod(rest) ClassName::instancemethod (arg0是ClassName类型的)
(args) -> expr.instancemethod(args) expr::instanceMethod

仔细总结就可以发现,能转换的基础就是二者的签名必须相同。

3、构造函数引用

以上面的Supplier和Function为例:

1
2
3
4
5
6
7
8
9
10
11
    Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
等价于:
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();

Function<Integer, Apple> f1 = Apple::new;
Apple a2 = f1.apply(110);
等价于:
Function<Integer, Apple> f1 = (weight) -> new Apple(weight);
Apple a2 = f1.apply(110);

四、Lambda实战

1、匿名类

1
2
3
4
5
6
7
8
List<Apple> list = new ArrayList<Apple>();

list.sort(new Comparator<Apple>(){
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
});

2、Lambda表达式

因为Comparator是函数式接口,所以可以直接传入Lambda表达式

1
2
3
List<Apple> list = new ArrayList<Apple>();

list.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

前面还介绍了类型推断,所以还可以再简化一下:

1
2
3
List<Apple> list = new ArrayList<Apple>();

list.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

在Comparator中有一个静态辅助方法,它接收一个Function来提取Comparable键值,并生成一个Comparator对象。

所以代码可以变成这样:

1
2
3
List<Apple> list = new ArrayList<Apple>();

list.sort(Comparator.comparing((a) -> a.getWeight()));

注意,这个Lambda表达式是作为一个Function类型的参数。

3、使用方法引用

方法引用就是替代那些转发参数的Lambda表达式的语法糖。所以代码最后就变成了这样:

1
2
3
List<Apple> list = new ArrayList<Apple>();

list.sort(Comparator.comparing(Apple::getWeight));