目标
了解使用匿名内部类存在的问题
体验
Lambda
使用匿名内部类存在的问题
当需要启动一个线程去完成任务时,通常会通过 Runnable
接口来定义任务内容,并使用
Thread
类来启动该线程。
传统写法
,
代码如下:
public class Demo01LambdaIntro {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override public void run() {
System.out.println("新线程任务执行!");
}
}).start();
}
}
由于面向对象的语法要求,首先创建一个 Runnable
接口的匿名内部类对象来指定线程要执行的任务内容,再将其交 给一个线程来启动。
代码分析
:
对于
Runnable
的匿名内部类用法,可以分析出几点内容:
- Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心
- 为了指定 run 的方法体,不得不需要 Runnable 接口的实现类
- 为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类
- 必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错
- 而实际上,似乎只有方法体才是关键所在。
Lambda
体验
Lambda
是一个
匿名函数
,可以理解为一段可以传递的代码。
Lambda
表达式写法
,
代码如下:
借助
Java 8
的全新语法,上述
Runnable
接口的匿名内部类写法可以通过更简单的
Lambda
表达式达到相同的效果
public class Demo01LambdaIntro {
public static void main(String[] args) {
new Thread(() -> System.out.println("新线程任务执行!")).start(); // 启动线程
}
}
这段代码和刚才的执行效果是完全一样的,可以在JDK 8
或更高的编译级别下通过。从代码的语义中可以看出:我们 启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
我们只需要将要执行的代码放到一个Lambda
表达式中,不需要定义类,不需要创建对象。
Lambda
的优点
简化匿名内部类的使用,语法更加简单。
小结
了解了匿名内部类语法冗余,体验了
Lambda
表达式的使用,发现
Lmabda
是简化匿名内部类的简写
Lambda
的标准格式
目标
掌握
Lambda
的标准格式
练习无参数无返回值的
Lambda
练习有参数有返回值的
Lambda
Lambda
的标准格式
Lambda
省去面向对象的条条框框,
Lambda
的标准格式格式由
3
个部分
组成:
格式说明:
- (参数类型 参数名称):参数列表
- {代码体;}:方法体
- -> :箭头,分隔参数列表和方法体
Lambda
与方法的对比
匿名内部类
public void run() {
System.out.println("aa");
}
Lambda
() -> System.out.println("bb!")
练习无参数无返回值的
Lambda
掌握了
Lambda的语法,我们来通过一
interface Swimmable {
public abstract void swimming();
}
package com.itheima.demo01lambda;
public class Demo02LambdaUse {
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<T>
接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1, T o2)
;
当需要对一个对象集合进行排序时, Collections.sort
方法需要一个 Comparator
接口实例来指定排序的规则。
传统写法
如果使用传统的代码对
ArrayList
集合进行排序,写法如下:
public class Person {
private String name;
private int age;
private int height;
// 省略其他
}
package com.itheima.demo01lambda;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
public class Demo03LambdaUse {
public static void main(String[] args) {
ArrayList<Person> persons = new ArrayList<>();
persons.add(new Person("刘德华", 58, 174));
persons.add(new Person("张学友", 58, 176));
persons.add(new Person("刘德华", 54, 171));
persons.add(new Person("黎明", 53, 178));
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);
}
}
}
这种做法在面向对象的思想中,似乎也是“理所当然”的。其中 Comparator
接口的实例(使用了匿名内部类)代表 了“按照年龄从小到大”的排序规则。
Lambda
写法
package com.itheima.demo01lambda;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
public class Demo03LambdaUse {
public static void main(String[] args) {
ArrayList<Person> persons = new ArrayList<>();
persons.add(new Person("刘德华", 58, 174));
persons.add(new Person("张学友", 58, 176));
persons.add(new Person("刘德华", 54, 171));
persons.add(new Person("黎明", 53, 178));
Collections.sort(persons, (o1, o2) -> {
return o1.getAge() - o2.getAge(); });
for (Person person : persons) {
System.out.println(person);
}
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
的实现原理
我们现在已经会使用
Lambda
表达式了。现在同学们肯定很好奇
Lambda
是如何实现的,现在我们就来探究
Lambda 表达式的底层实现原理。
@FunctionalInterface
interface Swimmable {
public abstract void swimming();
}
public class Demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(new Swimmable() {
@Override public void swimming() {
System.out.println("使用匿名内部类实现游泳");
}
});
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
我们可以看到匿名内部类会在编译后产生一个类: Demo04LambdaImpl$1.class
使用XJad反编译这个类,得到如下代码:
package com.itheima.demo01lambda;
import java.io.PrintStream;
// Referenced classes of package com.itheima.demo01lambda:
// Swimmable, Demo04LambdaImpl
static class Demo04LambdaImpl$1 implements Swimmable {
public void swimming() {
System.out.println("使用匿名内部类实现游泳");
}
Demo04LambdaImpl$1() { }
}
我们再来看看
Lambda
的效果,修改代码如下:
public class Demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(() -> { System.out.println("Lambda游泳");
});
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说Lambda
并没有在编译的时候产生
一 个新的类。使用XJad
对这个类进行反编译,发现
XJad
报错。使用了
Lambda
后
XJad
反编译工具无法反编译。我们使用 JDK自带的一个工具: javap
,对字节码进行反汇编,查看字节码指令。
在
DOS
命令行输入: javap -c -p 文件名
.class -c:表示对代码进行反汇编
-p:显示所有类和成员
反汇编后效果如下:
C:\Users\>javap -c -p Demo04LambdaImpl.class
Compiled from "Demo04LambdaImpl.java"
public class com.itheima.demo01lambda.Demo04LambdaImpl {
public com.itheima.demo01lambda.Demo04LambdaImpl();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:swimming:
()Lcom/itheima/demo01lambda/Swimmable;
5: invokestatic #3 // Method goSwimming:
(Lcom/itheima/demo01lambda/Swimmable;)V
8: return
public static void goSwimming(com.itheima.demo01lambda.Swimmable);
Code:
0: aload_0
1: invokeinterface #4, 1 // InterfaceMethod
com/itheima/demo01lambda/Swimmable.swimming:()V 6: return
private static void lambda$main$0();
Code:
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String Lambda游泳
5: invokevirtual #7 // Method java/io/PrintStream.println:
(Ljava/lang/String;)V
8: return
}
可以看到在类中多出了一个私有的静态方法 lambda$main$0
。这个方法里面放的是什么内容呢?我们通过断点调试 来看看:
可以确认
lambda$main$0
里面放的就是
Lambda
中的内容,我们可以这么理解
lambda$main$0
方法:
public class Demo04LambdaImpl {
public static void main(String[] args) { ... }
private static void lambda$main$0() {
System.out.println("Lambda游泳");
}
}
关于这个方法
lambda$main$0
的命名:以
lambda
开头,因为是在
main()
函数里使用了
lambda
表达式,所以带有 $main表示,因为是第一个,所以$0
。
如何调用这个方法呢?其实
Lambda
在运行的时候会生成一个内部类,为了验证是否生成内部类,可以在运行时加 上 -Djdk.internal.lambda.dumpProxyClasses ,加上这个参数后,运行时会将生成的内部类
class
码输出到一个文 件中。使用java
命令如下:
java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名
根据上面的格式,在命令行输入以下命令:
C:\Users\>java -Djdk.internal.lambda.dumpProxyClasses com.itheima.demo01lambda.Demo04LambdaImpl
Lambda
游泳
执行完毕,可以看到生成一个新的类,效果如下:
反编译 Demo04LambdaImpl$$Lambda$1.class 这个字节码文件,内容如下:
// Referenced classes of package com.itheima.demo01lambda:
// Swimmable, Demo04LambdaImpl
final class Demo04LambdaImpl$$Lambda$1 implements Swimmable {
public void swimming() {
Demo04LambdaImpl.lambda$main$0();
}
private Demo04LambdaImpl$$Lambda$1() {}
}
可以看到这个匿名内部类实现了
Swimmable
接口,并且重写了
swimming
方法,
swimming
方法调用 Demo04LambdaImpl.lambda$main$0() ,也就是调用
Lambda
中的内容。最后可以将
Lambda
理解为:
public class Demo04LambdaImpl {
public static void main(String[] args) {
goSwimming(new Swimmable() {
public void swimming() {
Demo04LambdaImpl.lambda$main$0();
}
});
}
private static void lambda$main$0() {
System.out.println("Lambda表达式游泳");
}
public static void goSwimming(Swimmable swimmable) {
swimmable.swimming();
}
}
小结
匿名内部类在编译的时候会一个class
文件
Lambda
在程序运行的时候形成一个类
1.
在类中新增一个方法,
这个方法的方法体就是
Lambda
表达式中的代码
2.
还会形成一个匿名内部类,
实现接口
,
重写抽象方法
3.
在接口的重写方法中会调用新生成的方法
.
Lambda
省略格式
目标
掌握
Lambda
省略格式
在
Lambda
标准格式的基础上,使用省略写法的规则为:
1.
小括号内参数的类型可以省略
2.
如果小括号内
有且仅有
一
个参数
,则小括号可以省略
3.
如果大括号内
有且仅有
一
个语句
,可以同时省略大括号、
return
关键字及语句分号
(int a) -> {
return new Person();
}
省略后
a -> new Person()
Lambda
的前提条件
目标
掌握
Lambda
的前提条件
Lambda
的语法非常简洁,但是
Lambda
表达式不是随便使用的,使用时有几个条件要特别注意:
1.
方法的参数或局部变量类型必须为接口才能使用
Lambda
2.
接口中有且仅有一个抽象方法
public interface Flyable {
public abstract void flying();
}
package com.itheima.demo01lambda;
public class Demo05LambdaCondition {
public static void main(String[] args) {
test01(() -> { });
Flyable s = new Flyable() {
@Override public void flying() { }
};
Flyable s2 = () -> { };
}
public static void test01(Flyable fly) {
fly.flying();
}
}
小结
Lambda
表达式的前提条件
:
1.
方法的参数或变量的类型是接口
2.
这个接口中只能有一个抽象方法
函数式接口
函数式接口在
Java
中是指:
有且仅有
一
个抽象方法的接口
。
函数式接口,即适用于函数式编程场景的接口。而
Java
中的函数式编程体现就是
Lambda
,所以函数式接口就是可以适用于Lambda
使用的接口。只有确保接口中有且仅有一个抽象方法,Java
中的
Lambda
才能顺利地进行推导。
FunctionalInterface
注解
与
@Override
注解的作用类似,
Java 8
中专门为函数式接口引入了一个新的注解: @FunctionalInterface
。该注 解可用于一个接口的定义上:
@FunctionalInterface
public interface Operator {
void myMethod();
}
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
Lambda
和匿名内部类对比
目标
了解
Lambda
和匿名内部类在使用上的区别
1.
所需的类型不一样
- 匿名内部类,需要的类型可以是类,抽象类,接口
- Lambda表达式,需要的类型必须是接口
2.
抽象方法的数量不一样
- 匿名内部类所需的接口中抽象方法的数量随意
- Lambda表达式所需的接口只能有一个抽象方法
3.
实现原理不同
- 匿名内部类是在编译后会形成class
- Lambda表达式是在程序运行的时候动态生成class
小结
当接口中只有一个抽象方法时,
建议使用
Lambda
表达式
,
其他其他情况还是需要使用匿名内部类