lambda表达式
1.lambda表达式概述
lambda表达式是Java8的新特性,也被称为箭头函数、匿名函数、闭包。这种表达式体现的是轻量级函数式编程的思想。
->
符号是lambda表达式的核心符号,符号左侧是操作参数,符号右侧是操作表达式语句块。lambda表达式出现的目的是在开发过程中对代码质量进行更好的控制,让编写的代码更加趋于数据的有效处理。也成为"编码及数据",Model code as data。
传统实现方式
传统的实现方式是接口&实现类
或 匿名内部类
。传统方式存在的问题如下,
- 语法冗余,存在很多与数据处理无关的代码
- this关键字指定存在很大误区
- 变量捕获,对于内部类型中当前作用域中变量的处理有特殊要求
以创建一个线程类为例,使用传统的匿名内部类的方式实现,
public class Demo1 {
public static void main(String[] args) {
// 传统模式创建新线程
new Thread(new Runnable() {
@override
void run() {
System.out.println("threading..." + Thread.currentThread.getId());
}
}).start();
}
}
上面的代码语法和执行上都是没有问题的,但是与数据相关的代码只有包含在run
方法的方法体中的那部分,其余冗余的代码通过lambda表达式的方式可以省去。
lambda表达式实现
public class Demo1 {
public static void main(String[] args) {
// lambda表达式优化线程模式
new Thread(() -> {
System.out.println("lambda threading..." + Thread.currentThread.getId());
}).start();
}
}
上方的代码中将冗余的代码省略。lambda表达式与传统方式相比对解决方案的语义进行了优化。
2.函数式接口
概念及特点
函数式接口的概念如下,
- 函数式接口就是Java类型系统中的接口
- 这种接口只含有一个接口方法,这一特点与其他接口区分
- 对于这种接口,Java8中提供了
@FunctionalInterface
的语义化注解进行验证
定义函数式接口示例
函数式接口本质上就是Java的接口,所以创建接口即可。以一个用户身份认证的接口为例,
@FunctionalInterface
public interface UserCredential {
/**
* 通过用户账号确定用户身份。name是用户账号,返回身份信息["系统管理员","用户管理员","普通用户"]
*/
String verifyUser(String name);
}
再定义另一个消息传输格式转换的接口,
@FunctionalInterface
public interface MessageFormat {
// 消息转换方法,message是要转换的消息,format是转换的格式。返回转换后的数据
String format(String message, String format);
}
默认接口方法和静态接口方法
默认方法和静态方法均可用于对接口功能的拓展。
1)默认接口方法
在之前的 UserCredential接口中添加默认接口方法,
@FunctionalInterface
public interface UserCredential {
String verifyUser(String name);
// 接口默认方法
default getCredential(String name) {
if ("admin".equals(name))
return "admin 系统管理员";
else if ("manager".equals(name))
return "manager 用户管理员";
return "common 普通用户";
}
}
默认方法的添加并不影响函数式接口的语义语法。因为函数式接口中依旧只有一个未实现的方法,默认方法不包含在其中。
对上一节笔记中定义的 UserCredential接口进行实现,定义接口实现类如下。
public class UserCredentialImp implements UserCredential {
public static void main(String[] args) {
// 1. 传统实现方式的调用
UserCredentialImp uci = new UserCredentialImp();
System.out.println(uci.verifyUser("daihdoa")); // 打印 "普通用户"
// 2. 使用接口默认方法
ic.getCredential("daihdoa")); // 打印 "common 普通用户"
// 3. 匿名内部类方式,实现接口抽象方法
UserCredential uc = new UserCredential() {
@override
public String verifyUser(String name) {
return "admin".equals(name)? "管理员": "会员";
}
};
uc.verifyUser("admin"); // 返回 "管理员"
}
// 传统实现
@override
public String verifyUser(String name) {
if ("admin".equals(name))
return "系统管理员";
else if ("manager".equals(name))
return "用户管理员";
return "普通用户";
}
}
2)静态接口方法
为之前的 MessageFormat接口添加静态方法,
@FunctionalInterface
public interface MessageFormat {
String format(String message, String format);
// 实现一个静态方法,用于验证消息是否合格
static boolean verifyMessage(String msg) {
if (msg != null)
return true;
return false;
}
}
静态方法的定义并不会对函数式接口的语义语法产生影响。
对上面的静态方法进行测试,
public class MessageFormatImp implements MessageFormat {
public static void main(String[] args) {
MessageFormatImp format = new MessageFormatImp();
String msg = "hello world";
if (MessageFormat.verifyMessage(msg)) {
format.format(msg, "json");
}
}
@override
public String format(String message, String format) {
System.out.println("消息转换。。。");
return message;
}
}
继承自Object类的方法
Java中所有的对象都继承Object类,从 Object类中继承过来的方法即使是抽象的,也不会影响函数式接口的语法语义。如,
@FunctionalInterface
public interface MessageFormat {
String format(String message, String format);
// 继承自Object类的抽象方法
String toString();
lambda表达式和函数式接口的关系
函数式接口只包含一个未实现的操作方法,Java8中提供的lambda表达式也只能操作一个方法。Java中的lambda表达式,核心就是一个函数式接口的实现。
会看上一节中 默认接口方法
的笔记,传统实现方式和匿名内部类的函数式接口实现方式都存在代码的冗余。如果使用lambda表达式可以只针对执行部分的代码进行书写。
通过lambda表达式进行 UserCredential接口实现,还是针对 默认接口方法
部分的代码进行书写,
public static void main(String[] args) {
// 1. 传统实现方式的调用
UserCredentialImp uci = new UserCredentialImp();
System.out.println(uci.verifyUser("daihdoa")); // 打印 "普通用户"
// 2. 使用接口默认方法
ic.getCredential("daihdoa")); // 打印 "common 普通用户"
// 3. 匿名内部类方式,实现接口抽象方法
UserCredential uc = new UserCredential() {
@override
public String verifyUser(String name) {
return "admin".equals(name)? "管理员": "会员";
}
};
uc.verifyUser("admin"); // 返回 "管理员"
// 使用lambda表达式实现函数式接口
UserCredential uc2 = (String name) -> {
return "admin".equals(name)? "lambda管理员": "lambda会员";
};
uc2.verifyUser("admin"); // 返回 "lambda管理员"
}
Java8 中常见函数式接口
Java8中接口,如 Runnable、Comparable、Comparator等,这些接口在创建之初并不是为了实现函数式接口而创建的。只是恰好符合了Java8中出现的函数式接口的特点,故可以通过 lambda表达式创建。
在Java8的java.util.function
包中提供了很多常用的函数式功能接口,这些接口大多支持泛型。以 Predicate<T>
接口、Supplier<T>
接口 和 Function<T, R>
接口为例,这3个接口的源码如下,
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
@FunctionalInterface
public interface Supplier<T> {
T get();
}
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
使用lambda表达式对该接口进行实现,
public static void main(String[] args) {
// 此处实现一个String类型的Predicate接口实现类对象
Predicate<String> pre = (String name) -> {
return "admin".equals(name);
};
System.out.println(pre.test("manager")); // 返回 false
// 此处实现一个String类型的Supplier接口实现类对象
Supplier<String> sup = () -> {
return UUID.randomUUID().toString();
};
System.out.println(sup.get()); // 返回一个随机UUID
// 此处使用lambda表达式实现一个Function接口,入参是String类型,结果是Integer类型
Function<String, Integer> func = (String gender) -> {
return "male".equals(gender)? 1: 0;
};
System.out.println(func.apply("female")); // 返回 0
}
这种带泛型的接口在使用lambda表达式定义时,需要声明类型,这些泛型作为方法的输入参数或者输出结果的类型指定。所以在使用内建的函数式接口时需要明确这泛型类型在接口方法中的作用(即明确是入参类型还是结果类型)。
3.lambda表达式基础语法
基本语法
lambda表达式的语法分为4个部分,
部分 | 说明 |
---|---|
声明 | 与lambda表达式绑定的接口名 |
参数 | 包含在一对圆括号中,和函数式接口中未实现的方法具有相同的参数个数和类型顺序 |
操作符 | -> |
执行代码块 | 包含在一对尖括号中,出现在操作符的左侧 |
声明接口名 变量名 = (参数) -> {执行代码块};
1)无参数且无返回值的lambda表达式
首先定义函数式接口,
@FunctionalInterface
public interface MyLambda1 {
void test();
}
在另一个Java程序中对该函数式接口进行实现,
public class MyLambda1Imp implements MyLambda1 {
public static void main(String[] args) {
MyLambda1 my1 = () -> {
......; // 无返回
};
my1.test();
// 当代码块只有一行代码时,可以省略尖括号
MyLambda1 my12 = () -> System.out.println("hello world");
my12.test();
}
}
2)有参数无返回值的lambda表达式
首先定义函数式接口,
@FunctionalInterface
public interface MyLambda2 {
void test(String name, int age);
}
在另一个Java程序中对该函数式接口进行实现,
public class MyLambda2Imp implements MyLambda2 {
public static void main(String[] args) {
// 标准方式
MyLambda2 my2 = (String n, int a) -> {
......; // 无返回
};
my2.test("a", 18);
// 这样写也是可以的,JVM会自动判断两个变量的类型
MyLambda2 my21 = (n, a) -> {
......; // 无返回
};
my2.test("a", 18);
}
}
3)有参数有返回值的lambda表达式
首先定义函数式接口,
@FunctionalInterface
public interface MyLambda3 {
int test(int x, int y);
}
在另一个Java程序中对该函数式接口进行实现,
public class MyLambda2Imp implements MyLambda2 {
public static void main(String[] args) {
MyLambda3 my3 = (a, b) -> {
......; // 返回一个 int类型的值
};
my3.test(12, 18);
// 当代码块只有一行代码时,可以省略尖括号和return语句
MyLambda3 my31 = (a, b) -> a + b;
my31.test(12, 18);
}
}
4)总结
- lambda表达式必须与函数式接口进行绑定
- lambda表达式的参数可以有0个或多个,括号中可以不指定参数类型,JVM在运行时会根据接口的抽象方法进行类型判断
- 有返回值的lambda表达式,如果代码块只有一行,可以省略尖括号和return语句。
变量捕获
变量捕获是表达式使用过程中对于所属作用域的变量的访问规则。相比于匿名内部类,lambda表达式在变量捕获方面进行了较大的优化。
1)匿名内部类变量捕获
匿名内部类的变量捕获使用 Runnable接口的匿名内部类实现进行演示,
public class Demo {
String s1 = "全局变量";
@Test
public void testInnerClass() {
String s2 = "局部变量";
new Thread( new Runnable() {
String s3 = "内部变量";
@override
public void run() {
// 访问全局变量,下面这种访问方式是会报错的
// 此处this表示的是内部类型对象,即Runnable对象
System.out.println(this.s1)
// 正确访问全局变量s1的方式
System.out.println(s1);
// 局部变量的访问同全局变量,不能对局部变量数据进行修改
// 匿名内部类中,局部变量被认为是final修饰的
System.out.println(s2);
// 内部变量访问,内部变量是可以直接修改的
System.out.println(s3);
System.out.println(this.s3);
}
}).start();
}
}
一定注意,在匿名内部类中使用this
关键字,指代的不是外部类型对象,而是匿名内部类所属的对象。
2)lambda表达式变量捕获
同样使用 Runnable接口的lambda表达式实现,
public class Demo {
String s1 = "全局变量";
@Test
public void testLambda() {
String s2 = "局部变量";
new Thread(() -> {
String s3 = "内部变量";
// 访问全局变量,可以直接使用this关键字
// this关键字就是所在方法的所属类型对象
System.out.println(this.s1)
// 局部变量的访问同全局变量,不能对局部变量数据进行修改
// 匿名内部类中,局部变量被认为是final修饰的
System.out.println(s2);
// 内部变量访问,内部变量是可以直接修改的
System.out.println(s3);
System.out.println(this.s3);
}).start();
}
}
lambda表达式可以直接通过this
关键字访问到所在方法的所属类对象的全局变量的原因是没有像匿名内部类那样创建run
方法的Runnable对象作用域。表达式本身就是所在的testLambda所属的Demo类对象作用域的一部分。
类型检查
lambda表达式类型检查可以分为两部分,表达式类型检查和参数类型检查。
首先定义函数式接口,
@FunctionalInterface
public interface MyInterface<T, R> {
R strategy(T t, R r);
}
在另一个程序中对该函数式接口进行实现,
public class Demo {
// 该方法的参数是MyInterface对象,此处已经指定类型
public static void test(MyInterface<String, List> inter) {
List<String> list = inter.strategy("hello", new ArrayList());
System.out.println(list);
}
public static void main(String[] args) {
// 匿名内部类实现方式
test(new MyInterface<String, List>() {
@override
public List strategy(String s, List list) {
return list.add(s);
}
});
// lambda表达式实现方式
test((s, list) -> {
return list.add(s);
});
}
}
1)表达式类型检查
目的是确定lambda表达式实现的是哪个函数式接口。
该过程很简单,以上方的代码为例,test
方法需要一个 MyInterface接口类作为参数,所以test方法括号中的 lambda表达式被JVM判定为 MyInterface类型对象。
2)参数类型检查
目的是检查lambda表达式的第二部分,即参数的类型。
- 通过表达式类型检查确定lambda表达式实现的是 MyInterface接口,就可以锁定到其实现的是该接口中
strategy
方法 - 该方法的参数类型有两个,分别是
T
和R
类型。在test
方法的定义中可以确定这两个类分别是 String和ArrayList - 进一步可以确定lambda表达式中
s
和list
分别代表上面的两个类型,在这一步推导中JVM会依据运行过程中上下文内容对lambda表达式进行检测,确保表达式的参数类型、数目和顺序一致
方法重载
首先定义2个函数式接口,用于作为传递给重载方法的参数,
@FunctionalInterface
public interface Param1 {
void outInfo(String info);
}
@FunctionalInterface
public void Param2 {
void outInfo(String info);
}
针对上面的两个接口定义重载方法,
public class Demo {
public void lambdaMethod(Param1 param) {
param.outInfo("hello");
}
public void lambdaMethod(Param2 param) {
param.outInfo("world");
}
public static void main(String[] args) {
Demo d = new Demo();
// 匿名内部类方式创建Param1并传入方法,可以正常执行
d.lambdaMethod(new Param1() {
@override
public void output(String info) {
System.out.println(info);
}
});
// 匿名内部类方式创建Param2并传入方法,可以正常执行
d.lambdaMethod(new Param2() {
@override
public void output(String info) {
System.out.println("-----Param2-----")
System.out.println(info);
}
});
// 使用lambda表达式进行调用,此处会报错
d.lambdaMethod((String msg) -> {
System.out.println(msg);
});
}
}
报错原因分析如下,
- lambda表达式存在类型检查,自动推导lambda表达式的目标类型
lambdaMethod
方法是重载方法,两个重载方法参数数目相同,其可使用的参数类型分别是 Param1和Param2,二者均为函数式接口- 这两个函数式接口的方法参数类型也相同,所以通过lambda表达式创建函数式接口对象时,JVM无法确定创建哪个接口对象
lambda表达式底层运行原理*
单独创建一个Java文件,
public class Demo {
public static void main(String[] args) {
MarkUp mu = (message) -> {
System.out.println(message);
};
mu.markUp("lbd");
}
}
@FunctionalInterface
interface MarkUp {
void markUp(String msg);
}
使用命令行对上方文件进行编译,
javac /path/to/Demo.java
编译后会生成Demo.class
和MarkUp.class
两份文件。查看Demo.class
文件的编译结果,
javap -p /path/to/Demo.class
返回结果中,Demo类存在3个方法,分别是构造方法、main
方法和lambda$main$0
方法。
其中lambda$main$0
方法是静态私有方法,对应的是lambda表达式所代表的方法。
通过下面的命令可以查看 Demo.java底层详细的编译过程,
java -Djdk.internal.lambda.dumpProxyClasses /path/to/Demo
命令执行后会构建一个Demo$$Lambda$1.class
字节码文件,通过之前的javap
命令查看该字节码文件的内容。其内容如下,
final class Demo$$Lambda$1 implements MarkUp {
private Demo$$Lambda$1();
public void markUp(java.lang.String);
}
JVM在进行编译时会创建一个类。
故此时 Demo.java经过编译后变为,
public class Demo {
public static void main(String[] args) {
new Demo$$Lambda$1().markUp();
}
private static void lambda$main$0(java.lang.String message) {
System.out.println(message);
}
final class Demo$$Lambda$1 implements MarkUp {
private Demo$$Lambda$1() {};
public void markUp(java.lang.String msg) {
Demo.lambda$main$0(msg);
};
}
}
总结如下,
- JVM在类中创建一个静态私有方法,该方法的方法体中的代码就是 lambda表达式执行代码块中的代码
- 针对 lambda表达式绑定的函数式接口定义一个被
final
关键字修饰的类型。该类型中会实现函数式接口的方法,该方法的实现是直接调用步骤1中生成的静态私有方法 - 之前的 lambda表达式转变为创建函数式接口实现类的实例对象并调用接口中定义的方法