JDK8~21新特性
前言
最近看到JDK21出来了,身为打工人,还是有点好奇JDK21的新特性,因此总结一下JDK8~21的所有的新特性,看看JDK筚路蓝缕的历程。
一、JDK8新特性
1.1 Lambda表达式
1.1.1 目标
了解使用匿名内部类型存在的问题,体验Lambda
1.1.2 使用匿名内部类存在的问题
当需要启动一个县城去完成任务时,通常会通过Runnable接口来定义任务内容,并使用 Thread 类来启动该线程。
传统写法,代码如下:
@Test
public void Demo01LambdaIntro(){
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行任务");
}
}).run();
}由于面向对象的语法要求,首先创建一个Runnable接口的匿名内部类对象来制定线程要执行的任务内容,再将其交给一个线程来启动。
代码分析:
对于 Runnable 的匿名内部类用法, 可以分析出几点内容:
- Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来制定线程任务内容的核心。
- 为了指定 run 的方法体,不得不需要 Runnable 接口的实现类。
- 为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类。
- 必须覆盖重写抽象 run 方法,所以方法名称,方法参数,方法返回值不得不再写一遍,且不能写错。
- 而实际上,似乎只有方法体才是关键所在。
1.1.3 Lambda 体验
Lambda 是一个匿名函数,可以理解为一段可以传递的代码。
Lambda 表达式写法,代码如下:
借助 Java8 的全新语法,上述 Runnable 接口的匿名内部类写法可以通过更简单的Lambda 表达式达到相同的效果
@Test
public void Demo01LambdaIntro1(){
new Thread(()->{
System.out.println("线程执行任务");
}).run();
}这段代码和刚才的执行效果完全一样的,可以在JDK8 或更高的编译级别下通过。从代码的语义中欧可以看出:我们启动类一个线程,而线程任务的内容以一种更加简洁的形式被指定。
我们只需要将要执行的代码放到一个Lambda表达式中,不需要定义类,不需要创建对象。
1.1.4 Lambda的优点
简化匿名内部类的使用,语法更加简单。
1.1.5 小结
了解匿名内部类语法冗余,体验了Lambda表达式的使用,发现 Lambda 是简化匿名内部类的简写。
1.2 Lambda的标准格式
- 掌握Lambda的标准格式
- 练习五参数无返回值的Lambda
- 练习有参数有返回值的Lambda
1.2.1 Lambda的标准格式
Lambda 省去面向对象的条条框框,Lambda的标准格式格式由 3 个部分组成:
(参数类型 参数名称) -> {
代码体;
}格式说明:
- (参数类型 参数名称):参数列表
- {代码体;}:方法体
->:箭头,分隔参数列表和方法体。
Lambda与方法的对比
匿名内部类
public void run(){
System.out.printle("aa");
}Lambda
() -> System.out.printle("bb!")1.2.2 练习无参数无返回值的Lambda
掌握了 Lambda 的语法,我们来通过一个案例熟悉 Lambda 的使用。
interface Swimmable {
public abstract void swimming();
}1.2.3 练习有参数有返回值的Lambda
下面举例演示 java.util.Comparator<T> 接口的使用场景代码,其中的抽象方法定义为:
public abstract int compare(T o1,T o2);
当需要对一个对象集合进行排序时,Collections.sort 方法需要一个 Comparator 接口实例来制定排序的规则。
传统写法
如果使用传统的代码对 ArrayList 集合进行排序,写法如下:
package com.yooome.boot.lambad;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
private int height;
}package com.yooome.boot.lambad;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class Demo03LambdaUse {
public static void main(String[] args) {
ArrayList<Person> personArrayList = new ArrayList<>();
personArrayList.add(new Person("1",21,21));
personArrayList.add(new Person("2",12,12));
personArrayList.add(new Person("3",13,13));
Collections.sort(personArrayList, new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
});
for (Person person : personArrayList) {
System.out.println(person);
}
}
}这种做法在面向对象的思想中,似乎也是 “理所当然” 的。其中 Comparator 接口的实例(使用了匿名内部类)代表了 按照年龄从小到大的排序规则。
Lambda写法
package com.yooome.boot.lambad;
import java.util.*;
import java.util.function.Consumer;
public class Demo03LambdaUse {
public static void main(String[] args) {
ArrayList<Person> personArrayList = new ArrayList<>();
personArrayList.add(new Person("1", 21, 21));
personArrayList.add(new Person("2", 12, 12));
personArrayList.add(new Person("3", 13, 13));
Collections.sort(personArrayList, (o1, o2) -> {
return o1.getAge() - o2.getAge();
});
for (Person person : personArrayList) {
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 表达式相当于是对接口中抽象方法的重写。
1.3 了解Lambda的实现原理
1.3.1 目标
了解 Lambda 的实现原理
我们现在已经回使用 Lambda 表达式了。现在同学们可定很好奇 Lambda 是如何实现的,现在我们就来探究 Lambda 表达式的底层实现原理。
@FunctionalInterface
interface Swimmable {
public abstract void 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(new Swimmable() {
@Override
public void swimming() {
System.out.println("使用匿名内部类实现游泳");
}
});
}
public static void goSwimming(Swimmable swimmable){
swimmable.swimming();
}
}运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说Lambda 并没有在编译的时候产生一个新的类。使用XJad对这个类进行反编译,发现XJad报错。使用了 Lambda 后 XJad 反编译工具无法反编译。我们使用JDK自带的一个工具: javap ,对字节码进行反编译,查看字节码指令。
在DOS命令行输入:
javap -c -p 文件名.class
-c: 表示对代码进行反汇编
-p: 显示所有类和成员反汇编效果如下:
yooome@192 lambad % javap -c -p Demo04LambdaImpl.class
Compiled from "Demo04LambdaImpl.java"
public class com.yooome.boot.lambad.Demo04LambdaImpl {
public com.yooome.boot.lambad.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 #7, 0 // InvokeDynamic #0:swimming:()Lcom/yooome/boot/lambad/Swimmable;
5: invokestatic #11 // Method goSwimming:(Lcom/yooome/boot/lambad/Swimmable;)V
8: return
public static void goSwimming(com.yooome.boot.lambad.Swimmable);
Code:
0: aload_0
1: invokeinterface #17, 1 // InterfaceMethod com/yooome/boot/lambad/Swimmable.swimming:()V
6: return
private static void lambda$main$0();
Code:
0: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #27 // String lambad 学会游泳了
5: invokevirtual #29 // 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) {
...
}
public 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.dumProxyClasses y要运行的包名.类名根据上面的格式,在命令行输入一下命令:
// 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();
}
}1.3.2 小结
匿名内部类在编译的时候会生成一个class文件
Lamdba 在程序运行的时候形成一个类
- 在类中新增一个方法,这个方法的方法体就是 Lambda 表达式中的代码
- 还会形成一个匿名内部类,实现接口,重写抽象方法
- 在接口的重写方法中会调用新生成的方法。
1.4 Lambda 省略格式
目标:
掌握 Lambda 省略格式
在 Lambda 标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略。
- 如果小括号内有且仅有一个参数,则小括号可以省略。
- 如果大括号内有且仅有一个语句,可以同事省略大括号,return 关键字及语句分号。
(int a) -> {
return new Person();
}省略后
a -> new Person()1.5 Lambda 的前提条件
掌握Lambda的前提条件
Lambda 的语法非常简洁,但是 Lambda 表示式不是随便使用的,使用时有几个条件要特别注意:
- 方法的参数或局部变量类型必须为接口才能使用 Lambda 。
- 接口中有且仅有一个抽象方法。
public interface Flyable {
public abstract void flying();
}1.6 小结
Lambad表达式的前提条件:
- 方法的参数或变量的类型是接口。
- 这个接口中只能有一个抽象方法。
1.7 函数式接口
函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的忌口。而且 Java 中的函数式编程体现就是 Lambda,所以函数式接口就是可以适用于 Lambda 会用的接口。只有确保接口中有且仅有一个抽象方法,Java 中的 :Lambda 才能顺利地进行推导。
FuncationlInterface 注解
与 @Override 注解的作用类似,Java8 中专门的函数式接口引入了一个新的注解:@FunctionalInterface。该注解可用于一个接口的定义上:
@FuncationlInterface
public interface Operator{
void myMethod();
}一旦使用该注解来定义接口,编译器将会强制检查该接口是否确定有且仅有一个抽象方法,否则将会报错。不过。,即时不适用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。
1.8 Lambda 和 匿名内部类对比
了解 Lambda 和匿名内部类在使用上的区别
所需的类型不一样
匿名内部类,需要的类型可以是类,抽象类,接口。
Lambda 表达式,需要的类型必须接口。
抽象方法的数量不一样
匿名内部类所需的接口中抽象的方法的数量随意。
Lambda 表达式所需的接口只能有一个抽象方法。
实现原理不同
匿名内部类是在编译后会形成class
Lambda 表达式是在程序运行的时候动态生成class
1.9 小结:
当接口中只有一个抽象方法时,建议使用Lambda表达式,其他其他情况还是需要使用匿名内部类。
二、JDK 8接口新增的两个方法
2.1 目标
了解 JDK8 接口新增的两个方法。
掌握接口默认方法的使用。
掌握接口静态方法的使用。
2.2 JDK8接口增强介绍
JDK 8 以前的接口:
interface 接口名{
静态常量;
抽象方法;
}JDK8 对接口的增强,接口还可以有默认的方法和静态方法
JDK8 的接口:
interface 接口名 {
静态常量;
抽象方法;
默认方法;
静态方法;
}接口引入默认方法的背景
在JDK8以前的接口中只能有抽象方法。存在以下问题:
如果给接口新增抽象方法,所有实现类都必须重写这个抽象方法。不利于接口的扩展。
interface A {
public abstrict void test1();
//接口新增抽象方法,所有实现类都需要去重写这个方法,非常不利于接口的扩展。
public abstrict void test2();
}
class B implements A {
@Voerride
public void test1(){
System.out.println("BB test1");
}
// 接口新增抽象方法,所有实现类都需要去重写这个方法
@Override
public void test2(){
System.out.println("BB test2");
}
}
class C implement A {
@Override
public void test1(){
System.out.println("cc test1");
}
// 接口新增抽象方法,所有实现类都需要去重写这个方法
@Override
public void test2(){
System.out.println("BB test2");
}
}例如,JDK 8 时在Map接口中增加了 forEach 方法:
public interface Map<K,V> {
abstrict void forEach(BiConsumer<? super K,? super V> action);
}通过API可以查询到Map接口的实现类如:

如果在Map接口中增加一个抽象方法,所有的实现类都需要去实现这个方法,那么工程量时巨大的。
因此,在JDK8时为接口新增了默认方法,效果如下:
public interface Map<K,V> {
default void forEach(BiConsumer<? super K,? super V> action){
...
}
}接口中的默认方法实现类不必重写,可以直接使用,实现类也可以分局需要重写。这样就方便接口的扩展。
2.3 接口默认方法的定义格式
interface 接口名{
修饰符 default 返回值类型 方法名(){
代码;
}
}2.4 接口默认方法的使用
方式一:实现类直接调用接口默认方法
方式二:实现类重写接口默认方法。
package com.yooome.boot.lambad;
public class Demo02UserDefaultFunction {
public static void main(String[] args) {
AA aa = new AA();
aa.test02();
CC cc = new CC();
cc.test02();
}
}
public interface BB {
void test1();
default void test02(){
System.out.println("AA test02 ");
}
}
public class AA implements BB{
@Override
public void test1() {
}
}
public class CC implements BB{
@Override
public void test1() {
}
@Override
public void test02() {
BB.super.test02();
}
}2.5 接口静态方法
为了方便接口扩展,JDK8位接口新增了静态方法
2.5.1 接口静态方法的定义格式
interface 接口名 {
修饰符 static 返回值类型 方法名(){
代码;
}
}2.5.2 接口静态方法的使用
直接使用接口名调用即可:接口名.静态方法名();
package com.yooome.boot.lambad;
public class Demo04UseStaticFunction {
public static void main(String[] args) {
// 直接使用接口名调用即可:接口名.静态方法名();
AAA.test01();
}
}
public interface AAA {
public static void test01(){
System.out.println("AAA 接口的静态方法");
}
}
public class BBB implements AAA{
// 静态方法不能重写
/*public static void test01(){
System.out.println("静态方法不能被重写");
}*/
}2.6 接口默认方法和静态方法的区别
- 默认方法通过实例调用,静态方法通过接口名调用。
- 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认的方法。
- 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。
2.7 小结
接口中新增的两种方法:
默认方法和静态方法
如何选择呢?如果这个方法需要被实现类继承或重写,使用默认方法,如果接口中的方法不需要被继承就使用静态方法。
三、常用内置函数式接口
3.1 目标
了解内置函数式接口由来
了解常用内置函数式接口
3.2 内置函数式接口来由来
我们知道使用Lambda表达式的提前是需要有函数式接口。而Lambda使用时不关系接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用 Lambda 方便,JDK 提供了大量常用的函数式接口。
public class Demo01UserFunctionalInterface {
public static void main(String[] args) {
method((arr) -> {
int sum = 0;
for (int n : arr) {
sum += n;
}
return sum;
});
}
private static void method(Operator op) {
int[] arr = {1, 2, 3, 4};
int sum = op.getSum(arr);
System.out.println("sum = " + sum);
}
}
@FunctionalInterface
interface Operator{
int getSum(int[] arr);
}3.3 常用内置函数式接口介绍
它们主要在 java.util.funcation 包中。下面是最常用的几个接口
- Supplier 接口
@FunctionalInterface
public interface Supplier<T>{
public abstract T get();
}- Consumer 接口
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
}- Function 接口
@FunctionalInterface
public interface Funcation<T,R> {
public abstract R apply(T t);
}- Predicate接口
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
}
predicate 接口用于做判断,返回 boolean 类型的值3.4 Suppplier接口
java.util.function.Supplier<T> 接口,它意味着"供给" , 对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}供给型接口,通过Supplier接口中的get方法可以得到一个值,无参有返回的接口。
使用Lambda表达式返回数组元素最大值
使用Supplier接口作为方法参数类型,通过Lamdba 表达式求出 int 数组中的最大值。提示:接口的泛型请使用 java.lang.Interget 类。
代码示例:
public class Demo05Supplier {
public static void main(String[] args) {
printMax(()->{
int[] arr = {10,30,20,300,200,100};
Arrays.sort(arr);
return arr[arr.length - 1];
});
}
private static void printMax(Supplier<Integer> supplier) {
int max = supplier.get();
System.out.println(max);
}
}3.5 Consumer接口
java.util.function.Consumer<T> 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
@FunctionalInterface public interface Consumer<T> {
public abstract void accept(T t);
}使用Lambda表达式将一个字符串转成大写和小写的字符串
Consumer消费型接口,可以拿到accept方法参数传递过来的数据进行处理, 有参无返回的接口。基本使用如:
public class Demo05Supplier {
public static void main(String[] args) {
printMax((String s)->{
System.out.println(s.toLowerCase());
});
}
private static void printMax(Consumer<String> consumer) {
consumer.accept("HelloWorld");
}
}