这是我参与 8 月更文挑战的第 21 天,活动详情查看: 8月更文挑战
概述
本文是在读过 Java8实战 后关于 Lambda 表达式的一些个人理解。Lambda表达式在Java8中引进,使Java适应了越来越流行的函数式编程,能够流式处理任务,也提供了一种更简洁的方式来替代匿名类。
可以在函数式接口上使用Lambda表达式
函数式接口与函数描述符
仅仅定义一个抽象方法的接口称之为函数式接口,抽象方法可以用Lambda表达式的写法进行简化,可将其称为函数描述符
被称作函数式接口的interface中有且只有一个方法,无论是它定义的还是从父类接口中继承下来的。Lambda表达式允许直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。 函数描述符是函数式接口中抽象方法的函数签名,由 (参数)->返回值 的形式组成,例如Runnable接口中的 void run() 方法,其函数描述符为 ()->void,因此使用Lambda表达式的形式实现Runnable接口时,只需要按照函数描述符的规范即可。如下所示
new Thread(() -> System.out.println("线程输出")) //由于返回值是void,因此箭头右边不需要return
可见使用 () -> System.out.println("线程输出") 就定义了一个标准的Runnable接口时实现类,因此 Runnable r = () -> System.out.println("线程输出") 是合法的,可见函数描述符实际上得到这个函数式接口的实现类
Java8中新增的函数式接口
Java8中新增的函数式接口如下,根据其函数描述符来看,包含了实际大部分的使用场合
| 函数式接口 | 使用案例 | 函数描述符 |
|---|---|---|
| Predicate | 布尔表达式 | T -> boolean |
| BiPredicate<L, R> | 布尔表达式 | (L,R) -> boolean |
| Consumer | 消费一个对象 | T -> void |
| BiConsumer<T,U> | 消费两个对象 | (T,U) -> void |
| Supplier | 创建一个对象 | () -> T |
| Function<T, R> | 一个对象转化成另一个对象 | T -> R |
| BiFunction<T, U, R> | 两个对象转化成另一个对象 | (T,U) -> R |
| UnaryOperator | 对Function的限制,参数和返回值类型相同 | T -> T |
| BiUnaryOperator | 对BiFunction的限制,参数和返回值类型相同 | (T,T) -> T |
原始类型的特化接口
由于泛型的限制,函数式接口对于原始类型大多使用了对应的引用类型来进行替代表示。例如一个判断数字是否为偶数的谓语如下
Predicate<Integer> predicate = x-> x % 2 == 0;
这样要判断一个int类型的数字需要经过自动装箱操作,这是耗费性能的。对于这种情况Java8提供了针对原始类型进行特化的函数式接口,例如IntPredicate
IntPredicate evenPredicate = x->x%2 == 0;
其他的接口也有类似的扩展,如DoubleConsumer, LongConsumer,DoubleSupplier,IntSupplier等
捕获变量
Lambda表达式可以使用外层作用域中定义的变量,包括实例变量和局部变量。但需要注意的是局部变量必须声明为final类型
由于局部变量是存在栈上的,有时Lambda表达式和定义局部变量的方法不在一个线程中,为防止局部变量所在的线程被回收,Lambda表达式会拷贝一份局部变量,这样的话外部局部变量再修改就是无意义的。因此需要被声明为final类型的。
方法引用
方法引用可以视作Lambda表达式的一个语法糖,这种写法进一步的简化Lambda表达式,使用ClassName::method()这种写法进行表示
构建方法引用的三种类型
指向静态方法
静态方法本身使用类名进行调用,例如 StringUtils.toUpperCase(String s),将字符串变为大写的静态方法,使用Lambda表达式的方法引用则可以将参数进行省略
List<String> colorsUpper = colors.stream().map(StringUtils::toUpperCase).collect(Collectors.toList());
指向任意类型的实例方法
对于诸如BiConsumer式的函数式接口,有时会把第二个对象作为第一个对象实例方法的参数,如下所示
BiConsumer<Print, Apple> biConsumer = (print, apple) -> print.getWeight(apple);
这种情况也可以使用方法引用进行简化,如下所示
BiConsumer<Print, Apple> biConsumer = Print::getWeight;
指向现有对象的实例方法
若T类型是实例有 method(V v) 方法,那么当有T类型的实例t之后,可以直接使用t::method来代替t.method(v)。对于调用现有对象的实例方法可以使用如下写法
Print print = new Print(); Consumer<Apple> consumer = print::getWeight;
构造函数引用
对于现有的构造函数可以使用Class::new来创建引用。适用于创建对象的函数式接口中,例如Supplier、Function等,它们的等价写法如下
Supplier<Apple> apples = Apple::new; // 等价new Apple
Function<Integer, Apple> function = Apple::new; // 等价new Apple(int weight)
// 将weights转化为apples
List<Apple> apples = weights.stream().map(Apple::new).collect(Collectors.toList());
复合Lambda表达式
可以将多个Lambda表达式进行复合使用,主要有比较器复合、谓词复合和函数复合三种,通过语句复合,可以讲复杂的逻辑简化处理,也是实现流式处理的方式。
Comparator接口比较器复合
比较器链
Comparator是一个比较器类,同时也是一个函数式接口,可以自定义比较器对集合集合进行排序。但有时希望使用多个排序规则,当第一个无法比较时采用第二个比较规则,例如将一些苹果先按照颜色在按照重量进行排序的比较器类
apples.sort(Comparator.comparing(Apple::getColor).thenComparing(Apple::getWeight));
逆序
默认的按照重量排序是由小到大的,如果我们要将比较器排序的顺序反转,可以使用reversed()方法进行反转
apples.sort(Comparator.comparing(Apple::getWeight).reversed());
Predicate接口谓词复合
Predicate中的 T -> boolean 用来判断对象是否满足谓词,例如下面两个谓词,分别是判断苹果的重量是否超过150g和颜色是否为红色
Predicate<Apple> heavyApple = x->x.getWeight()>150;
Predicate<Apple> redApple = x-> "red".equals(x.getColor());
如果需要获得一个重量超过150g且颜色为红色的谓词,就可以使用谓词复合的方式来组合。常见有与、或、非三种形式如下
Predicate<Apple> heaveyAndRed = heavyApple.and(redApple);
Predicate<Apple> heaveyOrRed = heavyApple.or(redApple);
Predicate<Apple> notRed = heavyApple.negate();
Function接口函数复合
Function<T,R>用来T类型转换为R类型,通常情况下我们会需要连续的处理,诸如复合函数f(g(x))f(g(x)),通常是先计算h=g(x)h=g(x),再计算f(h)f(h),可以使用Function接口的复合Lambda表达式进行处理。如下所示,得到的h2就是一个f(g(x)) 复合函数类
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
// g(f(x))
Function<Integer, Integer> h1 = f.andThen(g);
// f(g(x))
Function<Integer, Integer> h2 = f.compose(g);