前言
在开始之前,我们来看一个例子。
jdk8之前的代码(匿名类):
Comparator<Apple> appleComparator = new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
};
Jdk8的代码(Lambda表达式):
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一样,那么现在就可以直接使用它了,无需自己定义了。
举例:
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对象,无返回值。
示例:
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,将输入对象的信息映射到输出,就可以使用这个接口。
举例:
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)->boolean | |
BiConsumer | (T,U)->void | ObjIntConsumer,ObjLongConsumer,ObjDoubleConsumer |
BiFunction | (T,U)->R | ToIntBiFunction,ToLongBiFunction,ToDoubleBiFunction |
BinaryOperator | (T,T)->T | IntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator |
UnaryOperator | T->T | IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator |
类型检查、类型推断和限制
类型检查
Lambda表达式的类型是从使用Lambda的上下文推断出来的。上下文即为接受它传递的方法的参数或接受它的值的局部变量。上下文中Lambda表达式需要的类型为目标类型。
从调用的filter方法的参数找到函数式接口,由于每个函数式接口都只能有一个抽象方法,所以类型就都确定了。
类型推断
通过类型检查
我们可以知道函数描述符,所以就可以推断出lambda的签名,这样java编译器就可以推断出lambda表达式的参数类型,这样就可以在lambda表达式中省略参数类型了。
所以:
List<Apple> greenApples = filter(apples,(Apple a) -> "green".equals(a.getColor()));
可以简化为:
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
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
1.逆序
现在想逆序显示呢?
你不用再重新创建一个Comparator,因为Comparator提供了一个默认方法reverse()可以使给定的Comparator逆序。
因此,结合前面的Comparator即可。
apples.sort(comparing(Apple::getWeight).reversed());
2.比较器链
在上面的例子中,如果要比较的2个苹果的重量一样,那么哪个苹果应该排在前面呢?你可能需要再提供一个Comparator来进一步比较。比如,先根据重量排序,如果重量一样则再根据国家排序。
这个时候,Comparator中的thenComparing()方法就派上用场了。
Comparator<Apple> comparator = Comparator.comparing(Apple::getWeight);
apples.sort(comparator.reversed().thenComparing(Apple::getCountry));
谓词复合
谓词接口包括三个方法:negate、add和or,让你可以重用已有的Predicate来创建更复杂的谓词。
比如有如下谓词:
// 红苹果
Predicate<Apple> redApplePredicate = (Apple apple) -> "red".equals(apple.getColor());
// 不是红色的苹果
Predicate<Apple> nonRediApplePredicate = redApplePredicate.negate();
使用add()方法来创建一个既是红色又是一个重的苹果:
// 红苹果
Predicate<Apple> redApplePredicate = (Apple apple) -> "red".equals(apple.getColor());
Predicate<Apple> redAndWeightPredicate = redApplePredicate.and((Apple a) -> a.getWeight() > 150);
使用or()方法来创建一个150克以上的红苹果,或是绿苹果。
// 红苹果
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.
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))。
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