golang java8_Java8 新特性(一) - Lambda

本文介绍了Java8的重要特性——Lambda表达式,包括其引入的原因、功能和优势。Lambda允许以更简洁的方式处理函数接口,提高了代码的可读性和可维护性。通过举例展示了Lambda如何简化循环和条件判断,以及如何与Stream API结合使用。此外,还探讨了Lambda表达式与匿名类的区别以及其在字节码层面的实现。
摘要由CSDN通过智能技术生成

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

有疑问加站长微信联系(非本文作者)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值