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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容