Lambda表达式
Lambda表达式介绍
使用匿名内部类存在的问题,当需要启动一个线程去完成任务时,通常会通过 Runnable 接口来定义任务内容,并使用 Thread 类来启动该线程。
传统写法,代码如下:
package com.ll.lambda;
/**
* @Author 奥特曼
* @Date 2022/8/17 0017 21:26
* @Description TODO
**/
public class LambdaDemo {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程任务执行!");
}
}).start();
new Thread(() -> System.out.println("lambda线程任务执行!")).start();
}
public void run() {
System.out.println("aa");
}
}
由于面向对象的语法要求,首先创建一个 Runnable 接口的匿名内部类对象来指定线程要执行的任务内容,再将其交给一个线程来启动。
代码分析:
对于 Runnable 的匿名内部类用法,可以分析出几点内容:
Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心,为了指定 run 的方法体,不得不需要 Runnable 接口的实现类,为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类。必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错。
而实际上,似乎只有方法体才是关键所在。
Lambda体验
Lambda是一个匿名函数,可以理解为一段可以传递的代码。
Lambda表达式写法,代码如下:
借助Java 8的全新语法,上述 Runnable 接口的匿名内部类写法可以通过更简单Lambda表达式达到相同的效果。
这段代码和刚才的执行效果是完全一样的,可以在JDK 8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象。
Lambda的优点
简化匿名内部类的使用,语法更加简单。
Lambda的标准格式
Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成:
格式说明:
(参数类型 参数名称):参数列表
{代码体;}:方法体
-> :箭头,分隔参数列表和方法体
Lambda与方法的对比
匿名内部类中
public void run() {
System.out.println("aa");
}
Lambda中
() -> System.out.println("bb!")
练习无参数无返回值的Lambda
package com.ll.lambda;
public interface Swimmable {
public abstract void swimming();
}
package com.ll.lambda;
/**
* @Author 奥特曼
* @Date 2022/8/17 0017 21:32
* @Description TODO
**/
public class LambdaDemo1 {
public static void main(String[] args) {
goSwimming(new Swimmable() {
@Override
public void swimming() {
System.out.println("匿名内部类游泳");
}
});
goSwimming(() -> {
System.out.println("Lambda游泳");
});
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
练习有参数有返回值的Lambda
下面举例演示 java.util.Comparator 接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2);
当需要对一个对象集合进行排序时, Collections.sort 方法需要一个 Comparator 接口实例来指定排序的规则。
传统写法
如果使用传统的代码对 ArrayList 集合进行排序,写法如下:
package com.ll.lambda;
/**
* @Author 奥特曼
* @Date 2022/8/17 0017 21:34
* @Description TODO
**/
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}
package com.ll.lambda;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
/**
* @Author 奥特曼
* @Date 2022/8/17 0017 21:37
* @Description TODO
**/
public class LambdaDemo2 {
public static void main(String[] args) {
ArrayList<Person> persons = new ArrayList<>();
persons.add(new Person("刘德华", 58));
persons.add(new Person("张学友", 58));
persons.add(new Person("刘德华", 54));
persons.add(new Person("黎明", 53));
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
for (Person person : persons) {
System.out.println(person.getName()+": "+person.getAge());
}
}
}
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中 Comparator 接口的实例(使用了匿名内部类)代表“按照年龄从小到大”的排序规则。
Lambda写法:
package com.ll.lambda;
import java.util.*;
import java.util.function.Consumer;
/**
* @Author 奥特曼
* @Date 2022/8/17 0017 21:37
* @Description TODO
**/
public class LambdaDemo2 {
public static void main(String[] args) {
ArrayList<Person> persons = new ArrayList<>();
persons.add(new Person("刘德华", 58));
persons.add(new Person("张学友", 58));
persons.add(new Person("刘德华", 54));
persons.add(new Person("黎明", 53));
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
for (Person person : persons) {
System.out.println(person.getName() + ": " + person.getAge());
}
System.out.println("-----------------");
List<Integer> list = Arrays.asList(11, 22, 33, 44);
list.forEach(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
System.out.println("-----------------");
list.forEach((s) -> {
System.out.println(s);
});
}
}
以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写。
Lambda的实现原理
匿名内部类会在编译后产生一个类.
lambda运行程序,并没有出现一个新的类,也就是说Lambda并没有在编译的时候产生一个新的类。
使用XJad对这个类进行反编译,发现XJad报错。使用了Lambda后XJad反编译工具无法反编译。我们使用JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。
在DOS命令行输入:
javap -c -p 文件名.class
-c:表示对代码进行反汇编
-p:显示所有类和成员
lambda类中多出了一个私有的静态方法 lambda$main 0 可以确认 l a m b d a 0可以确认 lambda 0可以确认lambdamain 0 里面放的就是 L a m b d a 中的内容,我们可以这么理解 l a m b d a 0 里面放的就是Lambda中的内容,我们可以这么理解 lambda 0里面放的就是Lambda中的内容,我们可以这么理解lambdamain$0 方法:
关于这个方法 lambda$main
0
的命名:以
l
a
m
b
d
a
开头,因为是在
m
a
i
n
(
)
函数里使用了
l
a
m
b
d
a
表达式,所以带有
0 的命名:以lambda开头,因为是在main()函数里使用了lambda表达式,所以带有
0的命名:以lambda开头,因为是在main()函数里使用了lambda表达式,所以带有main表示,因为是第一个,所以$0。如何调用这个方法呢?其实Lambda在运行的时候会生成一个内部类,为了验证是否生成内部类,
可以在运行时加上 -Djdk.internal.lambda.dumpProxyClasses ,加上这个参数后,运行时会将生成的内部类class码输出到一个文件中。使用java命令如下:
java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名
小结
匿名内部类在编译的时候会形成一个class文件。
Lambda在程序运行的时候形成一个类。
- 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码。
- 还会形成一个匿名内部类,实现接口,重写抽象方法。
- 在接口的重写方法中会调用新生成的方法.。
Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略
- 如果小括号内有且仅有一个参数,则小括号可以省略
- 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
(int a) -> {
return new Person();
}
省略后
a -> new Person()
Lambda的前提条件
- 方法的参数或局部变量类型必须为接口才能使用Lambda
- 接口中有且仅有一个抽象方法
函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
FunctionalInterface注解与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上:
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
Lambda和匿名内部类对比
- 所需的类型不一样
匿名内部类,需要的类型可以是类,抽象类,接口
Lambda表达式,需要的类型必须是接口 - 抽象方法的数量不一样
匿名内部类所需的接口中抽象方法的数量随意
Lambda表达式所需的接口只能有一个抽象方法 - 实现原理不同
匿名内部类是在编译后会形成class
Lambda表达式是在程序运行的时候动态生成class
小结
当接口中只有一个抽象方法时,建议使用Lambda表达式,其他其他情况还是需要使用匿名内部类