美文网首页
Java8 新特性(一) - Lambda 表达式

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

作者: 不智鱼 | 来源:发表于2017-09-27 20:53 被阅读152次

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

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

    Lambda 表达式的渊源

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

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

    为什么需要 Lambda 表达式

    内循环 VS. 外循环

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

            List<Interger> 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<Integer>() {
                @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<Integer> numbers) {
        int total = 0;
        for (int number : numbers) {
            total += number;
        }
        return total;
    }
    

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

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

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

    public int sumAll(List<Integer> numbers, Predicate<Integer> 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<Integer> 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<T>  // 消费一个元素,无返回
    java.util.function.Supplier<T>  // 每次返回一个 T 类型的对象
    java.util.function.Predicate<T> // 输入一个元素,返回 boolean 值,常用于 filter
    java.util.function.Function<T,R> // 输入一个 T 类型元素,返回一个 R 类型对象
    

    Lambda 表达式与匿名类

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

    lambda 实现

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

    public class LambdaStudy004 {
        public void print() {
            List<Integer> 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<Integer> {
            @Override
            public void accept(Integer x) {
                LambdaStudy004Inner.lambda$print$0(x);
            }
        }
    
        public void print() {
            List<Integer> list = Arrays.asList(1, 2, 3, 4);
            list.forEach(new LambdaStudy004Inner().new lambda$1());
        }
    }
    

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

    相关文章

      网友评论

          本文标题:Java8 新特性(一) - Lambda 表达式

          本文链接:https://www.haomeiwen.com/subject/jklpextx.html