Java 8 效率精进指南(3)Lambda 表达式

53 阅读8分钟

宇宙正在为我敞开大门,给我带来机会。我不会在伟大的边缘崩溃。我抓住来的机会,自信地走过那些门。

5758486424584192.png

什么是 Lambda 表达式

匿名类可用于表示不同的行为,例如“过滤那些年龄在35岁以上的员工”、“过滤985、211的员工”,但这 仍然不够简洁,我们需要为这些过滤条件声明对应的类,只使用它们中的接口进行条件判断

Lambda 表达式是用来 替代匿名类 的一种精简的表达形式,它降低了只有一个接口匿名类的复杂程度,直接描述行为本身,能够提升代码可读性和简洁性。

Lambda 表达式的名字来源于希腊字母 λ,它鼓励人们采用行为参数化的方式简明传递行为。

Lambda 表达式的基本语法

Lambda 表达式的由三部分组成

  • 参数: 带有类型声明的参数列表
  • 箭头: ->
  • 表达式主体: 即函数体,可以有返回值,也可以没有返回值

image.png

以“筛选年龄大于35岁的员工”这一用例来讲,5行的代码可以直接精简成1行

// 筛选资深员工,匿名类写法
Comparator<Employee> byAge = new Comparator<Employee>() {
    public int compare(Employee e1, Employee e2) {
        return e1.getAge().compareTo(e2.getAge());
    }
}

// Lambda 写法
Comparator<Employee> byAge = (Employee e1, Employee e2) -> e1.getAge().compareTo(e2.getAge());

Lambda 表达式的几种变体写法

分成左右两侧来讨论,如下图所示:

image.png

为什么要用 Lambda 表达式

  • 代码 简洁性
  • 行为参数化 兼容良好
  • 流式处理 兼容性良好

Lambda 表达式的典型应用场景

所谓函数式接口,是指只定义一个抽象方法的接口

以下是 JDK 里常见的函数式接口。

// JDK 常见函数式接口

// java.util.Comparator
public interface Comparator<T> {
    int compare(T o1, T o2);
}

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

所有函数式接口的应用场景,都可以用 Lambda 表达式进行替代。

public static void process(Runnable r){
    r.run();
}

process(() -> System.out.println("Hello World 3")); // 使用 Lambda 表达式作为 Runnable 类型的参数

Lambda 表达式的应用场景总结

虽然 Lambda 表达式可以用在任何函数式接口场景下,但仍然有必要对其典型应用场景进行总结:

  • 从处理与对象的关系角度: 消费一个对象,生成一个对象。
  • 在消费一个对象时: 生成布尔值,提取任意信息。
  • 在处理多个对象时: 进行比较,组合两个值。
使用案例Lambda 示例
消费一个对象(Employee e) -> { System.out.println(e.name) }
生成一个对象() -> new Employee("Davlid")
生成布尔值(Employee e) -> e.age > 35
提取任意信息(Employee e) -> e.salary
进行比较(Employee e1, Employee e2) -> e1.age > e2.age
组合两个值(Employee e1, Employee e2) -> e1.salary + e2.salary

补充知识:java.util.function 包引入的几个函数式接口

java.util.function 包里面新引入了 ConsumerFunctionPredicate 等函数式接口,分别对应 消费一个对象由对象进行运算并返回结果由对象进行运算并返回 Boolean 结果

使用 Consumer

java.util.function.Consumer<T> 定义了名为 accept 的抽象方法,接收泛型 T 的对象,没有返回(或者说返回 void)。用于 访问类型 T 的对象,并对其执行某些操作 的场景。

以下代码对列表创建了 forEach 函数,在遍历列表时执行特定操作,例如打印列表中的每一个元素。

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

// 定义 forEach
public static <T> void forEach(List<T> list, Consumer<T> c) {
    for (T i: List) {
        c.accept(i);
    }
}

// 执行语句,遍历打印列表中的每一个元素
forEach(
    Arrays.asList(1,2,3,4,5),
    (Integer i) -> System.out.println(i)
);

使用 Function

java.util.function.Function<T, R> 作为 SAM,定义了名为 apply 的函数,用于 接受泛型T的对象,生成泛型R的对象,可以理解为 对象映射

接下来利用它创建了 map 函数,用于对列表中的元素执行某项操作,例如计算 String 字符串长度。

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

public static <T, R> List<R> map(List<T> list, Function<T,R> f) {
    List<R> result = new ArrayList<>();
    for (T t: list) {
        result.add(f.apply(t));
    }
    return result;
}

// 计算每个字符串长度,结果输出 [7,2,6]
List<Integer> l = map(
    Arrays.asList("lambdas", "in", "action"),
    (String s) -> s.length()
);

使用 Predictable

java.util.function.Predictable<T> 定义了 test 函数,由对象计算出布尔返回值,用于条件判断。

接口声明为泛型,在实际使用时,可以应用在任何需要的类型上。这里使用 Predicate 接口创建一个名为 filter 的函数,实现对列表的过滤操作。

@FunctionalInterface
public interface Predicate<T>{
    boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> results = new ArrayList<>();
    for(T s: list){
        if(p.test(s)){
            results.add(s);
        }
    }
    return results;
}

// 实现 test 函数,String -> Boolean
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();

// 在过滤器中使用 Predicate 行为(而非对象)
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

函数式接口的基本类型特化

在上文介绍的三个泛型函数式接口 Consumer<T>Function<T,R>Predicate<T> 中,均使用了 泛型 作为参数类型声明。在 Java 中存在两种对象:引用类型(例如 Byte、Integer、Object、List)和基本类型(例如 int、double、byte、char)。而泛型只能绑定到引用类型,这是由泛型内部实现方式决定的。因此,如果想要对基础类型的数据执行函数式操作,就必然伴随着装箱(boxing)行为,装箱和拆箱是自动完成的,但这并不意味着在使用它们时,我们没有付出代价。

装箱后的值本质上是把基础类型包裹起来,并保存在里,因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值

对此,Java 8 提供了针对基本类型特化后的函数式接口,例如 DoublePredicateIntConsumerLongBinaryOperatorIntFunction等。这些特化接口列举如下:

函数式接口函数描述符基本类型特化
PredicateT->booleanIntPredicate, LongPredicate, DoublePredicate
ConsumerT->voidIntConsumer, LongConsumer, DoubleConsumer
Function<T,R>T->RIntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction
Supplier() -> TBooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperatorT->TIntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator
BinaryOperator(T,T)->TIntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate<L,R>(L,R)->boolean
BiConsumer<T, U>(T,U)->voidObjIntConsuer, ObjLongConsumer, ObjDoubleConsumer
BiFunction<T,U,R>(T,U)->RToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U>

Lambda 表达式的类型处理

类型检查

编译器在判断一个 Lambda 表达式使用是否合理时,会从函数签名来进行匹配,Lambda 表达式的输入参数、输出参数应当与函数式接口相对应。这样才是一个正确使用的 Lambda 表达式。

同一个表达式可用于不同的函数式接口

例如以下三个函数式接口,接收两个 Employee 类型的入参,返回 boolean 结果,它们的实现对象可以是完全一样的 Lambda 表达式,很神奇。

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

ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

void 兼容规则,无视返回值

即使一个语句的执行结果有返回值,也可以将其套用在不需要返回值的 Lambda 语境中,例如在使用对象时的 Consumer,它需要一个 (T -> void) 表达式,我们用 list.add(element) 会返回 true or false 的插入结果,编译器会直接无视其结果。

以下两种写法都是正确的。

// Predicate 需要一个 boolean
Predicate<String> p = s -> list.add(s);
// Consumer 只需要 void
Consumer<String> b = s -> list.add(s);

类型推断:简化 Lambda 表达式的类型声明

编译器可以从上下文推断出,你写下的 Lambda 表达式适用于什么样的函数式接口,这意味着它可以推断出适合 Lambda 的签名,从而在编码时省略参数类型的声明。

例如我们要找出列表里的资深员工。

// 完整写法
List<Employee> seniorEmployees = filter(employeeList, (Employee e) -> e.age > 35)
// 简化写法
List<Employee> seniorEmployees = filter(employeeList, e -> e.age > 35)

迄今为止,我们看到的 Lambda 表达式箭头左侧的参数,都是由括号 () 包围的。而当 Lambda 仅有一个参数时,借助类型推断,该参数两边的括号也可以省略

方法引用

方法引用与 Lambda 相关,但又不完全一致。同样是 Java 8 引入的新语法,作用于行为参数化的场景,方法引用更加易读,感觉也更自然。

例如,使用方法引用对员工的工作经验进行排序,改进 Lambda 的写法:

// 使用 Lambda
employees.sort((Employee e1, Employee e2) -> e1.getExp().compareTo(e2.getExp()));
// 使用方法引用,借助 java.util.Comparator.comparing
employees.sort(comparing(Employee::getExp));

方法引用与 Lambda 之间的关系

使用方法引用的场景,都可以用 Lambda 代替,那么,我们为什么还需要方法引用呢?相比与 Lambda 描述具体的行为,方法引用为这种行为赋予“名称”,也就是方法名(函数名)。

  • Lambda 描述执行什么操作,是具体信息
  • 方法引用描述操作的名称,可以是抽象信息

方法引用本质上是根据已有的方法实现(我个人更习惯于使用“函数”而非“方法”来称呼 function/method),来创建对应的 Lambda 表达式。通过显式地指明方法名称,代码的可读性会得到提升。

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

方法引用的格式

目标引用 + 分隔符:: + 方法名

注意方法引用只需要指明方法名,不需要写括号,因为实际上并未对方法进行调用,无需括号。

方法引用的一些例子如下表:

Lambda等效方法引用
(Employee e) -> e.getExp()Employee::getExp
() -> Thread.currentThread().dumpStack()Thread.currentThread()::dumpStack
(str, i) -> str.substring(i)String::substring
(String s) -> System.out.println(s)System.out::pringln

方法引用的三种构建方式

根据分隔符::左侧的 引用目标 的不同,方法引用有三种构建方式。

引用目标例子理解
静态方法Integer::parseInt调用静态方法处理某参数
类的实例方法String::length该对象是 Lambda 的一个参数
对象实例方法someEmployee::getAge调用外部对象某方法

以上三种方法引用可以与 Lambda 表达式互相转化。

image.png

参考资料

  • Java 8 in Action