概述
JDK8 为我们带来了 Lambda 表达式和函数式接口,这一点在前文介绍 Stream 和 Collectors 的时候已有提及。通过使用这些特性,我们可以更简洁的创建匿名内部类,也可以将方法作为参数直接传入方法中调用。本文将就这两点简要的总结一下 Lambda 的使用。
一、函数式接口
我们知道,java 中允许将接口作为方法的参数类型,但是我们只能传入其实现类。实际开发中,有些接口的仅有少数的方法,并且往往其实现类只在特定的地方使用,为此专门去创建一个新的实现类其实是有点繁琐的,为此 JDK8 引入了函数式接口。
函数式接口有且仅有一个抽象方法,抽象方法允许有一个默认实现(实际上接口的默认实现也是 JDK8 的新特性)。当我们调用方法时,可以直接通过 Lambda 表达式直接以匿名内部类的形式去实现他的方法。表现为直接在参数小括号中: void test(param1, () -> System.out.print("hello world"))
当使用作为方法参数类型是,通过在接口上添加@FunctionalInterface
注解来声明。
我们举个例子:
1 | // 函数式接口 |
我们可以看到,实现的代码非常的简洁,对于一些不复杂的功能用起来非常方便,而如果使用原本的实现方式:
1 | TestInterface testInterface = new TestInterface() { |
Lambda 写法的方便简洁可见一斑。
二、Lambda 表达式
1.语法
Lambda 表达式,也可称为闭包。一个典型的表达式由一对圆括号和括号中的参数,一个横杠加箭头,和一对大括号组成:
1 | (int x, int y) -> { System.out.println("hello world")) } |
实际上,表达式并不是所有时候都需要写的那么标准:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
2.配合函数式接口使用
Lambda 表达式需要配合函数式接口使用。
对于函数式接口,我们可以像上文举的例子一样,自己创建一个函数式接口,也可以使用 java.util.function
包下已经提供好的函数式接口。
java.util.function
包提供了很多现成的接口,菜鸟教程上介绍的很详细,这里就不复制黏贴了。
现在,我们用用看:
1 | // 使用自己的函数式接口 |
我们可以发现,对于我们自己的定义的接口,唯一的抽象方法是test()
,所以放入实现以后实际上就是test()
方法的实现,而 IntFunction 通过表达式实现的就是apply()
方法。
其中,函数式接口总是有且仅有一个抽象方法,当作为参数使用的时候,通过 Lambda 表达式传入匿名方法默认就是实现这个唯一的抽象方法。另外,和正常的接口一样,函数式接口也允许拥有多个 default 修饰的默认实现方法。
3.变量作用域
Lambda 实际上可以理解为一个匿名的内部类,他可以访问外部的变量,但是不可以对变量做出改变:
1 | int i = 0; |
上述这段代码会报编译错误,他会提示 lambda 表达式中使用的变量应该是 final。当然,我们删去 i++
这行代码,让方法返回 i + 1
就没影响了。可以见我们并不需要加 final 也可以。以前 java 的匿名内部类在访问外部变量的时候,外部变量必须用 final 修饰。在JDK8 对这个限制做了优化,可以不用显示使用final修饰,编译器自己隐式当成 final 来处理。
4.表达式的 this
我们可以通过一个简单的例子来了解一下:
1 | public class BeanA { |
匿名内部类的 this 指向的是内部类自己,而 Lambda 表达式里的 this 实际上指向的是离他最近的那一层的外部类。
之所以这样,是因为当要编译 Lambda 表达式的时候,JVM会把 Lambda 表达式编译为一个在本类中的以lambda$+数字的方法。
值得一提的是,我们知道静态方法通过类调用,所以静态方法是获取不到 this 实例的,而Lambda 表达式会被编译为一个方法,如果表达式中使用了 this,那么就会编译为一个非静态方法,而未使用 this,就会编译为一个带 static 关键字的静态方法。
三、方法引用
1.语法
方法引用通过方法的名字来指向一个方法,他使 Lambada 更加简洁易懂。
方法引用的写法类似这样 类/实例::方法名
:
- 引用静态方法/成员方法:Class::StaticMethod 或 Class::Method
- 引用成员方法:Instance::Method
下面举个例子来说明一下这些引用方式的差异:
假如我们有这么一个类:
1 | public class BeanA { |
我们通过方法引用来使用这些方法:
1 | // 1.引用静态方法 |
2.方法引用中的this
我们可以在上述的例子中注意到一个很有趣的地方,成员方法使用了this,但是仍然可以通过跟静态一样的形式去调用,但是却需要传递一个方法参数,而通过实例去调用就不需要传入参数。
instanceMethod()
本身是个无参的方法,但是在第二中引用方式中却传递了 a 这个对象进去,我们是不是可以认为这就是被传入使用的 this?
我们再写一个方法作为对照:
1 | // 传入李四 |
我们可以看到,同样一个方法,传入了不同的实例,this 就指向了不同的实例。我们可以进一步推测,是不是跟 python 中的类的成员方法中的 self 一样,java 的成员方法也有一个 this 作为参数,只是平时编译器帮我们省略了呢?
1 | // 手动添加一个 this |
至此问题就明朗了,this 其实也是一个方法参数。