前言

在开始之前,我们来看一个例子。

jdk8之前的代码(匿名类):

1
2
3
4
5
6
Comparator<Apple> appleComparator = new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
};

Jdk8的代码(Lambda表达式):

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

上面2段代码是等价的。
一对比,发现使用Lambda表达式的代码真是简洁不少。

Lambda表达式的特性

  • 匿名
    没有像普通函数一样有一个明确的名称。

  • 函数
    和普通方法一样,都有参数列表、函数主体、返回值,还可能有要抛出的异常列表。

  • 传递
    Lambda表达式可以作为参数传递给方法或存储在变量中。

  • 简洁
    无需像匿名类那样写很多模板代码。

在上面的例子中,(Apple a1,Apple a2)中a1,a2即是参数,->用于区分参数列表和函数主体,a1.getWeight().compareTo(a2.getWeight());则是函数主体,执行结果即是返回值。

函数式接口

函数式接口就是只定义一个抽象函数的接口,比如《JDK8行为参数化传递代码》中定义的ApplePredicate即是函数式接口。Runnable、Callable、Comparator都是函数式接口。

函数式接口是干嘛的?
Lambda表达式允许直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。

函数描述符

函数式接口的抽象方法的前面基本上就是Lambda表达式的签名。这种抽象方法叫做函数描述符。例如,Runnable可以看做是一个没有参数也没有返回值的签名。因为它只有一个抽象方法run(),该方法不接受参数,也不返回(void)。(Apple a1,Apple a2) -> int就是一个接收2个Apple对象作为参数,并且返回int的函数。

只有在使用函数式接口的时候才能使用Lambda表达式。

使用函数式接口

函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。
所以,为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。
在java API中,Runnable、Callable、Comparator都是函数式接口。在java 8中,在java.util.function中还新加了一些新的函数式接口。

Predicate

java.util.function.Predicate定义了一个名为test的抽象方法,参数为泛型T对象,并返回一个Boolean。与我们在《JDK8行为参数化传递代码》中定义了ApplePredicate一样,那么现在就可以直接使用它了,无需自己定义了。
举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> result = new ArrayList<>();

if (null != list && !list.isEmpty()) {
for (T t : list) {
if (p.test(t)) {
result.add(t);
}
}
}
return result;
}

List<String> strings = Arrays.asList("string", "strings", "Strings", "String", "Str");
List<String> upperCaseStrings = filter(strings, s -> s.charAt(0) >= 65 && s.charAt(0) <= 90);

Consumer

java.util.function.Consumer定义了一个名为accept的抽象方法,参数为泛型T对象,无返回值。
示例:

1
2
3
4
5
6
7
8
9
10
private static <T> void consumerFilter(List<T> list, Consumer<T> c) {
if (null != list && !list.isEmpty()) {
for (T t:list) {
c.accept(t);
}
}
}

List<String> strings = Arrays.asList("string", "strings", "Strings", "String", "Str");
consumerFilter(strings,s -> System.out.println(s));

Function

java.util.function.Funciton定义了一个名为apply的抽象方法,接受泛型T对象,并返回一个R对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口。
举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static <T,R> Map<T,R> functionFilter(List<T> list, Function<T,R> f) {
Map<T,R> result = new HashMap<>();

if (null != list && !list.isEmpty()) {
for (T t:list) {
result.put(t,f.apply(t));
}
}
return result;
}

// key=原始字符串,value=字符串长度
Map<String,Integer> stringIntegerMap = functionFilter(strings,s -> s.length());
System.out.println(stringIntegerMap);

常用的函数式接口

函数式接口 函数描述符 原始类型特化
Predicate T->boolean IntPredicate,LongPredicate,DoublePredicate
Consumer T->void IntConsumer,LongConsumer,DoubleConsumer
Function T->R IntFunction,IntToDoubleFunction,LongFunction,IntToLongFunction,
LongToIntFunction,DoubleFunction,ToIntFunction,
ToDoubleFunction,ToLongFunction
Supplier ()->T BooleanSupplier,IntSupplier,LongSupplier,DoubleSupplier
BiPredicate<L,R> (L,R)->boolean
BiConsumer<T,U> (T,U)->void ObjIntConsumer,ObjLongConsumer,ObjDoubleConsumer
BiFunction<T,U,R> (T,U)->R ToIntBiFunction<T,U>,ToLongBiFunction<T,U>,ToDoubleBiFunction<T,U>
BinaryOperator (T,T)->T IntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator
UnaryOperator T->T IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator

类型检查、类型推断和限制

类型检查

Lambda表达式的类型是从使用Lambda的上下文推断出来的。上下文即为接受它传递的方法的参数或接受它的值的局部变量。上下文中Lambda表达式需要的类型为目标类型。
从调用的filter方法的参数找到函数式接口,由于每个函数式接口都只能有一个抽象方法,所以类型就都确定了。

类型推断

通过类型检查我们可以知道函数描述符,所以就可以推断出lambda的签名,这样java编译器就可以推断出lambda表达式的参数类型,这样就可以在lambda表达式中省略参数类型了。
所以:

1
List<Apple> greenApples = filter(apples,(Apple a) -> "green".equals(a.getColor()));

可以简化为:

1
List<Apple> greenApples = filter(apples,a -> "green".equals(a.getColor()));

但有时候显示的写出参数类型更易读,有时候去掉则更易读。需要根据实际情况作出选择。

方法引用

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

当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。比如Apple::getWeight就是引用了Apple类中定义的getWeight方法。但是,注意不需要括号,因为你还没有实际调用这个方法。方法引用时Lambda表达式的快捷写法。

Lambda和等效的方法引用的例子

函数式接口 函数描述符
(Apple a) -> a.getWeight() Apple::getWeight
(str,i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) System.out::println
() -> Thread.currentThread().dumpStack() Thread.currentThread::dumpStack

复合Lambda表达式的有用方法

许多函数式接口,比如Predicate、Function、Comparator都提供了进行复合的方法。这样你就可以将多个简单的Lambda表达式复合成更复杂的表达式。

比较器复合

有如下Comparator

1
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

1.逆序
现在想逆序显示呢?
你不用再重新创建一个Comparator,因为Comparator提供了一个默认方法reverse()可以使给定的Comparator逆序。
因此,结合前面的Comparator即可。

1
apples.sort(comparing(Apple::getWeight).reversed());

2.比较器链
在上面的例子中,如果要比较的2个苹果的重量一样,那么哪个苹果应该排在前面呢?你可能需要再提供一个Comparator来进一步比较。比如,先根据重量排序,如果重量一样则再根据国家排序。
这个时候,Comparator中的thenComparing()方法就派上用场了。

1
2
Comparator<Apple> comparator = Comparator.comparing(Apple::getWeight);
apples.sort(comparator.reversed().thenComparing(Apple::getCountry));

谓词复合

谓词接口包括三个方法:negate、add和or,让你可以重用已有的Predicate来创建更复杂的谓词。
比如有如下谓词:

1
2
3
4
// 红苹果
Predicate<Apple> redApplePredicate = (Apple apple) -> "red".equals(apple.getColor());
// 不是红色的苹果
Predicate<Apple> nonRediApplePredicate = redApplePredicate.negate();

使用add()方法来创建一个既是红色又是一个重的苹果:

1
2
3
// 红苹果
Predicate<Apple> redApplePredicate = (Apple apple) -> "red".equals(apple.getColor());
Predicate<Apple> redAndWeightPredicate = redApplePredicate.and((Apple a) -> a.getWeight() > 150);

使用or()方法来创建一个150克以上的红苹果,或是绿苹果。

1
2
3
// 红苹果
Predicate<Apple> redApplePredicate = (Apple apple) -> "red".equals(apple.getColor());
Predicate<Apple> redAndHeavyApplesOrGreen = redApplePredicate.and((Apple a) -> a.getWeight() > 150).or((Apple a) -> "green".equals(a.getColor()));

函数复合

你还可以将Function接口所代表的Lambda表达式复合起来。Function接口提供了addThen和compose2个默认方法,它们都会返回Function的一个实例。

比如,有一个函数f,给数字加1,另一个函数g给数字乘以2.你可以将它们组合成一个函数h,先给数字加1,再将结果乘以2.

1
2
3
4
5
Function<Integer,Integer> f = x -> x+1;
Function<Integer,Integer> g = x -> x*2;
Function<Integer,Integer> h = f.andThen(g);
int result = h.apply(1);
System.out.println(result); // 4

使用compose,先把给定的函数用作compose的参数里面的那个函数,然后再把函数本身用于结果。比如上面的例子使用compose的话,就是f(g(x)),而addThen则是g(f(x))。

1
2
3
4
Function<Integer,Integer> f = x -> x+1;
Function<Integer,Integer> g = x -> x*2;
Function<Integer,Integer>h = f.compose(g);
System.out.println(h.apply(1)); // 3