Java8 新特性(一) - Lambda 表达式

Java8 新特性(一) - Lambda 表达式

近些日子一直在使用和研究 golang,很长时间没有关心 java 相关的知识,前些天看到 java9 已经正式发布,意识到自己的 java 知识已经落后很多,心里莫名焦虑,决定将拉下的知识补上。

Lambda 表达式的渊源

Java8 作为近年来最重要的更新之一,为开发者带来了很多新特性,可能在很多其他语言中早已实现,但来的晚总比不来好。Lambda 表达式就是 Java8 带来的最重要的特性之一。

Lambda 表达式为 Java8 带来了部分函数式编程的支持。Lambda 表达式虽然不完全等同于闭包,但也基本实现了闭包的功能。和其他一些函数式语言不一样的是,Java 中的 Lambda 表达式也是对象,必须依附于一类特别的对象类型,函数式接口。

为什么需要 Lambda 表达式

内循环 VS. 外循环

先看一个非常简单的例子, 打印 list 内所有元素:

        List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)

        for (int number: bumbers) {
            System.out.println(number)
        }

作为一个 Java 开发者,你这一生可能已经写过无数次类似代码。看上去好像挺好的,没有什么需要改进的,我们显式的在外部迭代遍历 list 内元素,并挨个处理其中元素。那为什么提倡内部迭代呢,因为内部迭代有助于 JIT 的优化,JIT 可以将处理元素的过程并行化。

在 Java8 之前,需要借助 Guava 或其他第三方库来实现内部迭代,而在 Java8 中, 我们可以用以下代码实现:

        list.forEach(new Consumer() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });

以上代码还是稍显繁琐,需要创建一个匿名类,使用 lambda 表达式后,可以大大简化代码

        list.forEach((a) -> System.out.println(a));

Java 8 中 还引入了双冒号运算符,用于类方法引用,以上方法可以进一步简化为

        list.forEach(System.out::println);

内循环描述你要干什么,更符合自然语言描述的逻辑

passing behavior,not only value

通过 lambda 表达式,我们可以在传参时,不仅可以将值传入,还可将相关行为也传入,这样可以实现更加抽象和通用,更易复用的 API。看一下代码例子,需要实现一个求 list 内所有元素和的方法,嗯,看上去很简单。

public int sumAll(List numbers) {
    int total = 0;
    for (int number : numbers) {
        total += number;
    }
    return total;
}

这个时候,又有需求实现一个 list 内所有偶数和的方法,简单,代码复制一遍,稍作修改。

public int sumAllEven(List numbers) {
    int total = 0;
    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    }
    return total;
}

也没发多少功夫,还需要改进么,这个时候又需要所有奇数和呢,不同的需求过来,你需要一遍又一遍的复制代码。有没有更加优雅的解决方法呢?我们又想起了我们的 lambda 表达式,java 8 引入了一个新的函数接口 Predicate, 使用它来定义 filter,代码如下

public int sumAll(List numbers, Predicate p) {
    int total = 0;
    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

这样以上两个方法都可以通过这个方法实现,并且可以非常容易的扩展,当你需要用其他条件实现元素筛选求和时,只需要实现筛选条件的 lambda 表达式,如下

        System.out.println(sumAll(list, (a)-> true));           \\ 所有元素和
        System.out.println(sumAll(list, (a) -> a % 2 == 0));    \\ 所有偶数和
        System.out.println(sumAll(list, (a) -> a % 2 != 0));    \\ 所有奇数和

有同学会说,以前不用 lambda 表达式我们用接口也能实现。没错,用接口 + 匿名类也能实现类似效果,但 lambda 表达式更加直观,代码简捷,可读性也强,开发者也更有动力使用类似代码。

利于写出优雅可读性更高的代码

先看一段代码:

        List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
        
        for (int number : list) {
            if (number % 2 == 0) {
                int n2 = number * 2;
                if (n2 > 5) {
                    System.out.println(n2);
                    break;
                }
            }
        }

这个代码也不难理解,取了 list 中的偶数,乘以 2 后 大于 5 的第一个数,这个代码看上去不难,但是当你在实际业务代码中添加更多的逻辑时,就会显得可读性较差。使用 Java 8 新加入的 stream api 和 lambda 表达式重构这段代码后,如下

        System.out.println(
                list.stream()
                        .filter((a) -> a % 2 == 0)
                        .map((b) -> b * 2)
                        .filter(c -> c > 5)
                        .findFirst()
        );

一行代码就实现了以上功能,并且可读性也好,从做至右依次读过去,先筛选 偶数,在乘以 2, 再筛选大于 5 的数,取第一个数。并且 stream api 都是惰性的api,且不占用多余的空间,比如上面这段代码,并不会把list 中所有元素都遍历,当找到第一个符合要求的元素后就会停止。

Lambda 表达式语法

Lambda 表达式的语法定义在 Java 8 规范 15.27 中,并给出了一些例子

() -> {}                    // 无参数,body 为空
() -> 42                    // 无参数,表达式的值作为返回
() -> {return 42;}          // 无参数,block 块
() -> {System.gc();}
() -> {
    if (true) return 23;
    else {
        return 14
    }
}
(int x) -> {return x + 1;}  // 有参数,且显式声明参数类型
(int x) -> x + 1            
(x) -> x + 1                // 有参数,未显式声明参数类型,编译器推断参数类型
x -> x + 1          
(int x, int y) -> x + y
(x, y) -> x + y         
(x, int y) -> x + y         // 非法, 参数类型显示指定不能混用

总结一下:

  • Lambda 表达式可以具有零个,一个或多个参数。
  • 可以显式声明参数的类型,也可以由编译器自动从上下文推断参数类型。
  • 参数用小括号括起来,用逗号分隔。例如 (a, b) 或 (int a, int b) 或 (String a, int b, float c)
  • 空括号用于表示一组空的参数。
  • 当仅有一个参数时,且不显式指明类型,则可省略小括号
  • Lambda 表达式的正文可以包含零条,一条或多条语句。
  • 如果 Lambda 表达式的正文只有一条语句,则大括号可不用写
  • 如果 Lambda 表达式的正文有一条以上的语句必须包含在代码块中

Functional Interface (函数接口)

还有一个问题,在上面的内容没有提到,怎样在声明的时候表示 Lambda 表达式呢?比如函数可以接受一个Lambda表达式作为输入。Java 8 引入了一种新的概念,叫函数接口。其实说起来也不是什么新鲜东西,函数接口就是一种只包含一个抽象方法的接口(可以包含其他默认方法),同时 Java 8 引入一个新的注解 @FunctionalInterface,虽然不使用 FunctionalInterface 注解也可以使用,但是使用注解可以显式的声明该接口为函数接口,并且当接口不符合函数接口要求时,在编译期间抛出错误。之前 Java 已有的很多接口加上了该注解,最常见的比如 Runnable

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

也就是说,现在启动一个线程时,可以采用新的 Lambda 表达式

new Thread(
    () -> System.out.println("hello world")
).start()

之前已经存在的接口还有

java.lang.Comparable
java.util.concurrent.Callable

Java 8 中还新加了一些函数接口

java.util.function.Consumer  // 消费一个元素,无返回
java.util.function.Supplier  // 每次返回一个 T 类型的对象
java.util.function.Predicate // 输入一个元素,返回 boolean 值,常用于 filter
java.util.function.Function // 输入一个 T 类型元素,返回一个 R 类型对象

Lambda 表达式与匿名类

看上面的内容,一定会有人认为这些功能我使用匿名类也可以实现,那 Lambda 表达式和匿名类有什么区别呢。最明显的区别就是 this 指针,this 指针在匿名类中代表是匿名类,而在 Lambda 表达式中为包含 Lambda 表达式的类。同时,匿名类可以实现多个方法,而 Lambda 表达式只能有一个方法。
直观上,很多人会觉得 Lambda 表达式可能只是一个语法糖,最终转换为一个匿名类。事实上,考虑到实现效率问题,和向前兼容问题,Java 8 并没有采用匿名类语法糖,也没有和其他语言一样,采用专门的函数处理类型来实现 lambda 表达式。

lambda 实现

既然 lambda 表达式并未用匿名类的方式实现,那其原理到底是什么呢,之前我们分析泛型的时候都是分析字节码,这里也一样。我们先看一段代码和字节码。

public class LambdaStudy004 {
    public void print() {
        List list = Arrays.asList(1, 2, 3, 4);
        list.forEach(x -> System.out.println(x));
    }
}

javap -p 结果

public class lambda.LambdaStudy004 {
  public lambda.LambdaStudy004();
  public void print();
  private static void lambda$print$0(java.lang.Integer);
}

很明显,lambda 表达式编译后,会生成类的一个私有静态方法,然而,事情并没有那么简单,虽然生成了一个静态方法,lambda 表达式本身又由什么表示呢,java 中没有函数指针,总要有一个类作为载体调用该静态方法。

javap -p -v 查看字节码

...

37: invokedynamic #5,  0              // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
42: invokeinterface #6,  2            // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)
47: return

...

和普通的 static 方法调用采用 invokestatic 指令不一样,lambda 表达式的调用采用了 java 7 新引入的 invokedynamic 指令,该指令是为了加强 java 的动态语言特性引入,当 invokedynamic 指令被调用时,会调用 metafactory 函数动态生成一个实现了函数接口的对象,该对象实现的方法实际调用了之前生成的 static 方法,这个对象才是 lambda 表达式的实际翻译后的表示,翻译代码如下

class LambdaStudy004Inner {
    private static void lambda$print$0(Integer x) {
        System.out.println(x);
    }

    private class lambda$1 implements Consumer {
        @Override
        public void accept(Integer x) {
            LambdaStudy004Inner.lambda$print$0(x);
        }
    }

    public void print() {
        List list = Arrays.asList(1, 2, 3, 4);
        list.forEach(new LambdaStudy004Inner().new lambda$1());
    }
}

具体引入 invokedynamic 实现 Lambda 表达是的原因可以看 R 大的解释, 传送门: Java 8的Lambda表达式为什么要基于invokedynamic

你可能感兴趣的:(Java8 新特性(一) - Lambda 表达式)