前提
回想一下,JDK8
是2014年发布正式版的,到现在为(2020-02-08
)止已经过去了5年多。JDK8
引入的两个比较强大的新特性是Lambda
表达式(下文的Lambda
特指JDK
提供的Lambda
)和Stream
,这两个强大的特性让函数式编程在Java
开发中发扬光大。这篇文章会从基本概念、使用方式、实现原理和实战场景等角度介绍Lambda
的全貌,其中还会涉及一些函数式编程概念、JVM
一些知识等等。
基本概念
下面介绍一些基本概念,一步一步引出Lambda
的概念。
函数式接口
函数式接口和接口默认方法都是JDK8
引入的新特性。函数式接口的概念可以从java.lang.FunctionalInterface
注解的API
注释中得知:
An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification.
Conceptually, a functional interface has exactly one abstract method. Since {@linkplain java.lang.reflect.Method#isDefault() default methods} have an implementation, they are not abstract.
简单来说就是:@FunctionalInterface
是一个提供信息的接口(其实就是标识接口),用于表明对应的接口类型声明是一个Java
语言规范定义的函数式接口。从概念上说,一个函数式接口有且仅有一个抽象方法,因为接口默认方法必须予以实现,它们不是抽象方法。
所以可以这样给函数式接口定义:如果一个接口声明的时候有且仅有一个抽象方法,那么它就是函数式接口,可以使用@FunctionalInterface
注解标识。
JDK
中已经定义了很多内置的函数式接口,例如:
// java.lang.Runnable @FunctionalInterface public interface Runnable { public abstract void run(); } // java.util.function.Supplier @FunctionalInterface public interface Supplier<T> { T get(); } 复制代码
也可以自定义函数式接口,例如:
@FunctionalInterface public interface CustomFunctionalInterface { // 可以缩写为void process(); 接口方法定义的时候,默认使用public abstract修饰 public abstract void process(); } 复制代码
接口默认方法
接口默认方法的含义可以见Java
官方教程中对应的章节,在文末的参考资料可以查看具体的链接:
Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.
简单来说就是:默认方法允许你在你的类库中向接口添加新的功能,并确保新增的默认方法与这些接口的较早版本编写的代码二进制兼容。
接口默认方法(下称默认方法)通过default
关键字声明,可以直接在接口中编写方法体。也就是默认方法既声明了方法,也实现了方法。这一点很重要,在默认方法特性出现之前,Java
编程语言规范中,接口的本质就是方法声明的集合体,而自默认方法特性出现之后,接口的本质也改变了。默认方法的一个例子如下:
public interface DefaultMethod { default void defaultVoidMethod() { } default String sayHello(String name) { return String.format("%s say hello!", name); } static void main(String[] args) throws Exception { class Impl implements DefaultMethod { } DefaultMethod defaultMethod = new Impl(); System.out.println(defaultMethod.sayHello("throwable")); // throwable say hello! } } 复制代码
如果继承一个定义了默认方法的接口,那么可以有如下的做法:
- 完全忽略父接口的默认方法,那么相当于直接继承父接口的默认方法的实现(方法继承)。
- 重新声明默认方法,这里特指去掉
default
关键字,用public abstract
关键字重新声明对应的方法,相当于让默认方法转变为抽象方法,子类需要进行实现(方法抽象)。 - 重新定义默认方法,也就是直接覆盖父接口中的实现(方法覆盖)。
结合前面一节提到的函数式接口,这里可以综合得出一个结论:函数式接口,也就是有且仅有一个抽象方法的接口,可以定义0个或者N(N >= 1)个默认方法。这一点正是Stream
特性引入的理论基础。举个例子:
@FunctionalInterface public interface CustomFunctionalInterface { public abstract void process(); default void defaultVoidMethod() { } default String sayHello(String name) { return String.format("%s say hello!", name); } } 复制代码
这里说点题外话。
在写这篇文章的时候,笔者想起了一个前同事说过的话,大意如下:在软件工程中,如果从零做起,任何新功能的开发都是十分简单的,困难的是在兼容所有历史功能的前提下进行新功能的迭代。试想一下,Java
迭代到今天已经过去十多年了,Hotspot VM
源码工程已经十分庞大(手动编译过OpenJDK Hotspot VM
源码的人都知道过程的痛苦),任何新增的特性都要向前兼容,否则很多用了历史版本的Java
应用会无法升级新的JDK
版本。既要二进制向前兼容,又要迭代出新的特性,Java
需要进行舍夺,默认方法就是一个例子,必须舍去接口只能定义抽象方法这个延续了多年在Java
开发者中根深蒂固的概念,夺取了基于默认方法实现构筑出来的流式编程体系。笔者有时候也在思考:如果要我去开发Stream
这个新特性,我会怎么做或者我能怎么做?
嵌套类(Nested Classes)
嵌套类(Nested Classes
),简单来说就是:在一个类中定义另一个类,那么在类内被定义的那个类就是嵌套类,最外层的类一般称为封闭类(Enclosing Class
)。嵌套类主要分为两种:静态嵌套类和非静态嵌套类,而非静态嵌套类又称为内部类(Inner Classes
)。
// 封闭类 class OuterClass { ... // 静态嵌套类 static class StaticNestedClass { ... } // 内部类 class InnerClass { ... } } 复制代码
静态嵌套类可以直接使用封闭的类名称去访问例如:OuterClass.StaticNestedClass x = new OuterClass.StaticNestedClass();
,这种使用形式和一般类实例化基本没有区别。
内部类实例的存在必须依赖于封闭类实例的存在,并且内部类可以直接访问封闭类的任意属性和方法,简单来说就是内部类的实例化必须在封闭类实例化之后,并且依赖于封闭类的实例,声明的语法有点奇特:
public class OuterClass { int x = 1; static class StaticNestedClass { } class InnerClass { // 内部类可以访问封闭类的属性 int y = x; } public static void main(String[] args) throws Exception { OuterClass outerClass = new OuterClass(); // 必须这样实例化内部类 - 声明的语法相对奇特 OuterClass.InnerClass innerClass = outerClass.new InnerClass(); // 静态嵌套类可以一般实例化,形式为:封闭类.静态嵌套类 OuterClass.StaticNestedClass staticNestedClass = new OuterClass.StaticNestedClass(); // 如果main方法在封闭类内,可以直接使用静态嵌套类进行实例化 StaticNestedClass x = new StaticNestedClass(); } } 复制代码
内部类中有两种特殊的类型:本地类(Local Classes
)和匿名类(Anonymous Classes
)。
本地类是一种声明在任意块(block
)的类,例如声明在代码块、静态代码块、实例方法或者静态方法中,它可以访问封闭类的所有成员属性和方法,它的作用域就是块内,不能在块外使用。例如:
public class OuterClass { static int y = 1; { // 本地类A class A{ int z = y; } A a = new A(); } static { // 本地类B class B{ int z = y; } B b = new B(); } private void method(){ // 本地类C class C{ int z = y; } C c = new C(); } } 复制代码
匿名类可以让代码更加简明,允许使用者在定义类的同时予以实现,匿名类和其他内部类不同的地方是:它是一种表达式,而不是类声明。例如:
public class OuterClass { interface In { void method(String value); } public void sayHello(){ // 本地类 - 类声明 class LocalClass{ } // 匿名类 - 是一个表达式 In in = new In() { @Override public void method(String value) { } }; } } 复制代码
如果用Java
做过GUI
开发,匿名类在Swing
或者JavaFx
的事件回调中大量使用,经常会看到类似这样的代码:
JButton button = new JButton(); button.addActionListener(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { System.out.println("按钮事件被触发..."); } }); 复制代码
嵌套类的类型关系图如下:
Nested Classes - Static Nested Classes - None Nested Classes - Local Classes - Anonymous Classes - Other Inner Classes 复制代码
Lambda表达式
下面是来自某搜索引擎百科关于Lambda
表达式的定义:
Lambda
表达式(Lambda Expression
)是一个匿名函数,Lambda
表达式基于数学中的λ
演算得名,直接对应于其中的Lambda
抽象(Lambda Abstraction
),是一个匿名函数,即没有函数名的函数。Lambda
表达式可以表示闭包(注意和数学传统意义上的不同)。
Java
中的Lambda
表达式(下面称Lambda
)表面上和上面的定义类似,本质也是匿名函数,但其实现原理区别于一般的匿名类中的匿名函数实现,她是JDK8
引入的一颗新的语法糖。
引入Lambda表达式的初衷
如果一个接口只包含一个方法,那么匿名类的语法会变得十分笨拙和不清楚,产生大量的模板代码,归结一下就是:代码冗余是匿名类的最大弊端。在编程的时候,我们很多时候希望把功能作为参数传递到另一个方法,Lambda
就是为此而生,Lambda
允许使用者将功能视为方法参数,将代码视为数据。引入Lambda
带来了如下优势:
- 简化代码,引入了强大的类型推断和方法引用特性,简单的功能甚至可以一行代码解决,解放匿名类的束缚。
- 把功能作为参数向下传递,为函数式编程提供了支持。
至此还得出一个结论:Lambda
只适用于函数式接口对应唯一抽象方法的实现。
Lambda表达式的语法定义
Lambda
语法的详细定义如下:
// en_US InterfaceType interfaceObject = [Method Argument List] -> Method Body // zh_CN 接口类型 接口实例 = [方法参数列表] -> 方法体 复制代码
更具体的描述应该是:
接口类型 接口实例临时变量 = (方法参数类型X 方法参数类型X临时变量 , 方法参数类型Y 方法参数类型Y临时变量...) -> { 方法体... return 接口抽象方法返回值对应类型类型实例;}
一个Lambda
表达式由五个部分组成:
- 返回值:接口类型以及接口类型对应的临时实例变量。
- 等号:
=
。 - 方法参数列表:一般由中括号
()
包裹,格式是(类型1 类型1的临时变量,...,类型N 类型N的临时变量)
,在方法没有重载可以明确推断参数类型的时候,参数类型可以省略,只留下临时变量列表。特殊地,空参数列表用()
表示,如果参数只有一个,可以省略()
。 - 箭头:
->
。 - 方法体:一般由花括号
{}
包裹,格式是{方法逻辑... return 函数式接口方法返回值类型的值;}
,有几点需要注意:
- 如果方法体是空实现,用
{}
表示,如Runnable runnable = () -> {};
。 - 如果函数式接口抽象方法的返回值为
void
类型,则不需要return
关键字语句,如Runnable runnable = () -> {int i=0; i++;};
。 - 如果函数式接口抽象方法的方法体仅仅包含一个表达式,则不需要使用
{}
包裹,如Runnable runnable = () -> System.out.println("Hello World!");
。
举一些例子:
// Function - 具体 java.util.function.Function<String, Integer> functionY = (String string) -> { return Integer.parseInt(string); }; // Function - 简化 java.util.function.Function<String, Integer> functionX = string -> Integer.parseInt(string); // Runnable - 具体 Runnable runnableX = () -> { System.out.println("Hello World!"); }; // Runnable - 简化 Runnable runnableY = () -> System.out.println("Hello World!"); // 整数1-100的和 - 具体 int reduceX = IntStream.range(1, 101).reduce(0, (int addend, int augend) -> { return addend + augend; }); // 整数1-100的和 - 简化 int reduceY = IntStream.range(1, 101).reduce(0, Integer::sum); 复制代码
目标类型与类型推断
先引入下面的一个场景:
// club.throwable.Runnable @FunctionalInterface public interface Runnable { void run(); static void main(String[] args) throws Exception { java.lang.Runnable langRunnable = () -> {}; club.throwable.Runnable customRunnable = () -> {}; langRunnable.run(); customRunnable.run(); } } 复制代码
笔者定义了一个和java.lang.Runnable
完全一致的函数式接口club.throwable.Runnable
,上面main()
方法中,可以看到两个接口对应的Lambda
表达式的方法体实现也是完全一致,但是很明显最终可以使用不同类型的接口去接收返回值,也就是这两个Lambda
的类型是不相同的。而这两个Lambda
表达式返回值的类型是我们最终期待的返回值类型(expecting a data type of XX
),那么Lambda
表达式就是对应的被期待的类型,这个被期待的类型就是Lambda
表达式的目标类型。
为了确定Lambda
表达式的目标类型,Java
编译器会基于对应的Lambda
表达式,使用上下文或者场景进行综合推导,判断的一个因素就是上下文中对该Lambda
表达式所期待的类型。因此,只能在Java
编译器能够正确推断Lambda
表达式目标类型的场景下才能使用Lambda
表达式,这些场景包括:
- 变量声明。
- 赋值。
- 返回语句。
- 数组初始化器。
Lambda
表达式函数体。- 条件表达式(
condition ? processIfTrue() : processIfFalse()
)。 - 类型转换(Cast)表达式。
Lambda
表达式除了目标类型,还包含参数列表和方法体,而方法体需要依赖于参数列表进行实现,所以方法参数也是决定目标类型的一个因素。
方法参数的类型推导的过程主要依赖于两个语言特性:重载解析(Overload Resolution
)和参数类型推导(Type Argument Inference
)。
原文:For method arguments, the Java compiler determines the target type with two other language features: overload resolution and type argument inference
重载解析会为一个给定的方法调用(Method Invocation
)寻找最合适的方法声明(Method Declaration
)。由于不同的声明具有不同的签名,当Lambda
表达式作为方法参数时,重载解析就会影响到Lambda
表达式的目标类型。编译器会根据它对该Lambda
表达式的所提供的信息的理解做出决定。如果Lambda
表达式具有显式类型(参数类型被显式指定),编译器就可以直接使用Lambda
表达式的返回类型;如果Lambda
表达式具有隐式类型(参数类型被推导而知),重载解析则会忽略Lambda
表达式函数体而只依赖Lambda
表达式参数的数量。
举个例子:
// 显式类型 Function<String, String> functionX = (String x) -> x; // 隐式类型 Function<String, Integer> functionY = x -> Integer.parseInt(x); 复制代码
如果依赖于方法参数的类型推导最佳方法声明时存在二义性(Ambiguous
),我们就需要利用转型(Cast
)或显式Lambda
表达式来提供更多的类型信息,从而Lambda
表达式的目标类型。举个例子:
// 编译不通过 Object runnableX = () -> {}; // 编译通过 - Cast Object runnableY = (Runnable) () -> {}; // 静态方法入参类型是函数式接口 public static void function(java.util.function.Function function) { } function((Function<String, Long>) (x) -> Long.parseLong(x)); 复制代码
作用域
关于作用域的问题记住几点即可:
<1>
:Lambda
表达式内的this
引用和封闭类的this
引用相同。<2>
:Lambda
表达式基于词法作用域,它不会从超类中继承任何变量,方法体里面的变量和它外部环境的变量具有相同的语义。<3>
:Lambda expressions close over values, not variables
,也就是Lambda
表达式对值类型封闭,对变量(引用)类型开放(这一点正好解释了Lambda
表达式内部引用外部的属性的时候,该属性必须定义为final
)。
对于第<1>
点举个例子:
public class LambdaThis { int x = 1; public void method() { Runnable runnable = () -> { int y = this.x; y++; System.out.println(y); }; runnable.run(); } public static void main(String[] args) throws Exception { LambdaThis lambdaThis = new LambdaThis(); lambdaThis.method(); // 2 } } 复制代码
对于第<2>
点举个例子:
public class LambdaScope { public void method() { int x = 1; Runnable runnable = () -> { // 编译不通过 - Lambda方法体外部已经定义了同名变量 int x = 2; }; runnable.run(); } } 复制代码
对于第<3>
点举个例子:
public class LambdaValue { public void method() { (final) int x = 1; Runnable runnable = () -> { // 编译不通过 - 外部值类型使用了final x ++; }; runnable.run(); } } public class LambdaValue { public void method() { (final) IntHolder holder = new IntHolder(); Runnable runnable = () -> { // 编译通过 - 使用了引用类型 holder.x++; }; runnable.run(); } private static class IntHolder { int x = 1; } } 复制代码
方法引用
方法引用(Method Reference
)是一种功能和Lambda
表达式类似的表达式,需要目标类型和实现函数式接口,但是这个实现形式并不是通过方法体,而是通过方法名称(或者关键字)关联到一个已经存在的方法,本质是编译层面的技术,旨在进一步简化Lambda
表达式方法体和一些特定表达式的实现。方法引用的类型归结如下:
类型 | 例子 |
静态方法引用 | ClassName::methodName |
指定对象实例方法引用 | instanceRef::methodName |
特定类型任意对象方法引用 | ContainingType::methodName |
超类方法引用 | supper::methodName |
构造器方法引用 | ClassName::new |
数组构造器方法引用 | TypeName[]::new |
可见其基本形式是:方法容器::方法名称或者关键字
。
举一些基本的使用例子:
// 静态方法引用 public class StaticMethodRef { public static void main(String[] args) throws Exception { Function<String, Integer> function = StaticMethodRef::staticMethod; Integer result = function.apply("10086"); System.out.println(result); // 10086 } public static Integer staticMethod(String value) { return Integer.parseInt(value); } } // 指定对象实例方法引用 public class ParticularInstanceRef { public Integer refMethod(String value) { return Integer.parseInt(value); } public static void main(String[] args) throws Exception{ ParticularInstanceRef ref = new ParticularInstanceRef(); Function<String, Integer> function = ref::refMethod; Integer result = function.apply("10086"); System.out.println(result); // 10086 } } // 特定类型任意对象方法引用 String[] stringArray = {"C", "a", "B"}; Arrays.sort(stringArray, String::compareToIgnoreCase); System.out.println(Arrays.toString(stringArray)); // [a, B, C] // 超类方法引用 public class SupperRef { public static void main(String[] args) throws Exception { Sub sub = new Sub(); System.out.println(sub.refMethod("10086")); // 10086 } private static class Supper { private Integer supperRefMethod(String value) { return Integer.parseInt(value); } } private static class Sub extends Supper { private Integer refMethod(String value) { Function<String, Integer> function = super::supperRefMethod; return function.apply(value); } } } // 构造器方法引用 public class ConstructorRef { public static void main(String[] args) throws Exception { Function<String, Person> function = Person::new; Person person = function.apply("doge"); System.out.println(person.getName()); // doge } private static class Person { private final String name; public Person(String name) { this.name = name; } public String getName() { return name; } } } // 数组构造器方法引用 Function<Integer, Integer[]> function = Integer[]::new; Integer[] array = function.apply(10); System.out.println(array.length); // 10 复制代码