--- title: JDK8 特性(一) date: 2022-05-23 sidebar: 'auto' tags: - JDK8 categories: - Java --- ## 1 Lambda表达式 ### 1.1 Lambda标准格式 - Lambda省去面向对象的条条框框, Lambda的标准格式格式由3个部分组成 - 格式说明: - `(...args)`: 参数列表 - `->`: 分隔或连接参数与方法体的标识符 - `{...;}`: 方法体, 主要的代码逻辑 - eg: ```java class Demo{ public static void main(String[] args) { new Thread(() -> { System.out.println("线程启动"); }).start(); } } ``` ### 1.2 Lambda初体验 #### 无参无返回值 ```java public class Demo1 { public static void main(String[] args) { fishDo(() -> System.out.println("小鱼在欢乐的游着...")); } private static void fishDo(Fish fish){ fish.swim(); } } /** * 定义一个接口, 并且该接口只有一个需要实现的方法 **/ interface Fish{ void swim(); } ``` #### 有参有返回值 ```java public class Demo1 { public static void main(String[] args) { catEat(foodName -> { System.out.println("小猫在吃" + foodName); return 3; }); } private static void catEat(Cat cat){ System.out.println("小猫吃了" + cat.eat("🐟") + "分钟"); } } /** * 定义一个接口, 并且该接口只有一个需要实现的方法 **/ interface Cat{ int eat(String foodName); } ``` #### 小结 以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写 ### 1.3 Lambda实现原理 现有以下类 - 接口类 ```java public interface Swimable { void swimming(); } ``` - main入口 ```java public class SwimImplDemo { public static void main(String[] args) { goSwimming(new Swimable() { @Override public void swimming() { System.out.println("去匿名内部类游泳了"); } }); } private static void goSwimming(Swimable swim){ swim.swimming(); } } ``` - 将以上的main方法进行执行后, 会编译生成以下字节码文件 - ![](/java8/匿名内部类生成的字节码.png) - 将内部类的字节码文件通过 [XJad] 进行反编译 - 匿名内部类在编译后会形成一个新的类.$ ```java // Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://kpdus.tripod.com/jad.html // Decompiler options: packimports(3) fieldsfirst ansi space // Source File Name: SwimImplDemo.java package com.zhuhjay.lambda; import java.io.PrintStream; // Referenced classes of package com.zhuhjay.lambda: // Swimable, SwimImplDemo static class SwimImplDemo$1 implements Swimable{ public void swimming(){ System.out.println("去匿名内部类游泳了"); } SwimImplDemo$1(){} } ``` - main入口改为使用Lambda表达式 ```java public class SwimImplDemo { public static void main(String[] args) { goSwimming(() -> { System.out.println("去Lambda游泳了"); }); } private static void goSwimming(Swimable swim){ swim.swimming(); } } ``` - 将以上的main方法进行执行后, 不会生成多余的字节码文件 - 使用 [XJad] 反编译工具会失败 - 使用JDK工具来对Lambda表达式的字节码进行反汇编 ```text javap -c -p 文件名.class -c:表示对代码进行反汇编 -p:显示所有类和成员 ``` - 对Lambda表达式的字节码文件进行反汇编 ```S public class com.zhuhjay.lambda.SwimImplDemo { public com.zhuhjay.lambda.SwimImplDemo(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: invokedynamic #2, 0 // InvokeDynamic #0:swimming:()Lcom/zhuhjay/lambda/Swimable; 5: invokestatic #3 // Method goSwimming:(Lcom/zhuhjay/lambda/Swimable;)V 8: return private static void goSwimming(com.zhuhjay.lambda.Swimable); Code: 0: aload_0 1: invokeinterface #4, 1 // InterfaceMethod com/zhuhjay/lambda/Swimable.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表达式会在类中新生成一个私有的静态方法, 命名为`lambda$方法名$序列` - 通过断点调试Lambda也可以从调用栈中发现该静态方法的生成(当然不会显式的在源码中出现) - 验证在Lambda表达式运行中会生成一个内部类 - 命令格式: `java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名` - 执行(需要退出包运行该命令): `java -Djdk.internal.lambda.dumpProxyClasses com.zhuhjay.lambda.SwimImplDemo` - ![](/java8/获得运行时Lambda内部类.png) - 查看反编译结果 ```java final class SwimImplDemo$$Lambda$1 implements Swimable { public void swimming() { SwimImplDemo.lambda$main$0(); } private SwimImplDemo$$Lambda$1() {} } ``` - 对以上结论来推断Lambda生成的字节码文件 ```java public class SwimImplDemo { public static void main(String[] args) { // 也是相当于Lambda生成了一个匿名内部类, 来调用Lambda生成的静态方法 goSwimming(new Swimable() { public void swimming(){ SwimImplDemo.lambda$main$0(); } }); } /** Lambda生成的私有静态方法 **/ private static void lambda$main$0(){ System.out.println("去Lambda游泳了"); } private static void goSwimming(Swimable swim){ swim.swimming(); } } ``` - 小结 - 匿名内部类在编译的时候会一个class文件 - Lambda在程序运行的时候形成一个类 1. 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码 2. 还会形成一个匿名内部类, 实现接口, 重写抽象方法 3. 在接口的重写方法中会调用新生成的方法. ### 1.4 Lambda省略格式 在Lambda标准格式的基础上,使用省略写法的规则为: 1. 小括号内参数的类型可以省略 2. 如果小括号内有且仅有一个参数,则小括号可以省略 3. 如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号 ### 1.5 Lambda前提条件 Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意: 1. 方法的参数或局部变量类型必须为接口才能使用Lambda 2. 接口中有且仅有一个抽象方法(@FunctionalInterface用来检测接口是否为函数式接口) ### 1.6 Lambda和匿名内部类对比 1. 所需的类型不一样 - 匿名内部类,需要的类型可以是类,抽象类,接口 - Lambda表达式,需要的类型必须是接口 2. 抽象方法的数量不一样 - 匿名内部类所需的接口中抽象方法的数量随意 - Lambda表达式所需的接口只能有一个抽象方法 3. 实现原理不同 - 匿名内部类是在编译后会形成class - Lambda表达式是在程序运行的时候动态生成class ## 2 JDK8接口增强 ### 2.1 增强介绍 - JDK8以前的接口: ```text interface 接口名 { 静态常量; 抽象方法; } ``` - JDK8的接口: ```text interface 接口名 { 静态常量; 抽象方法; 默认方法; 静态方法; } ``` ### 2.2 接口默认方法和静态方法的区别 1. 默认方法通过实例调用,静态方法通过接口名调用。 2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。 3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。 ## 3 常用内置函数式接口 ### 3.1 内置函数式接口的由来 我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。 ### 3.2 常用函数式接口的介绍 它们主要在`java.util.function`包中。下面是最常用的几个接口。 #### Supplier:生产者 `java.util.function.Supplier`接口,它意味着"供给", 对应的Lambda表达式需要"对外提供"一个符合泛型类型的对象数据。 ```java @FunctionalInterface public interface Supplier { public abstract T get(); } ``` #### Consumer:消费者 `java.util.function.Consumer`接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。 ```java @FunctionalInterface public interface Consumer { public abstract void accept(T t); /** 该方法使得两个Consumer先后调用 c1.andThen(c2).accept(t) **/ default Consumer andThen(Consumer after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; } } ``` - 默认方法: andThen - 如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen - 要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是"一步接一步"操作 #### Function:类型转换 `java.util.function.Function`接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。 ```java @FunctionalInterface public interface Function { public abstract R apply(T t); default Function compose(Function before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } } ``` - 默认方法: andThen - Function 接口中有一个默认的 andThen 方法,用来进行组合操作 - 该方法同样用于"先做什么,再做什么"的场景,和 Consumer 中的 andThen 差不多 #### Predicate:判断 有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用`java.util.function.Predicate`接口。 ```java @FunctionalInterface public interface Predicate { public abstract boolean test(T t); /** && **/ default Predicate and(Predicate other) { Objects.requireNonNull(other); return (t) -> test(t) && other.test(t); } /** ! **/ default Predicate negate() { return (t) -> !test(t); } /** || **/ default Predicate or(Predicate other) { Objects.requireNonNull(other); return (t) -> test(t) || other.test(t); } static Predicate isEqual(Object targetRef) { return (null == targetRef) ? Objects::isNull : object -> targetRef.equals(object); } } ``` - 默认方法: and - 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用"与"逻辑连接起来实现"并且"的效果时,可以使用default方法 and - 默认方法: or - 与 and 的"与"类似,默认方法 or 实现逻辑关系中的"或" - 默认方法: negate - "与"、"或"已经了解了,剩下的"非"(取反)也会简单。它是执行了test方法之后,对结果boolean值进行"!"取反而已。一定要在 test 方法调用之前调用 negate 方法 ## 4 方法引用 方法引用的注意事项 1. 被引用的方法,参数要和接口中抽象方法的参数一样 2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值 ### 4.1 方法引用简化Lambda 使用Lambda表达式求一个数组的和 ```java public class Demo2 { public static void main(String[] args) { // 将数组进行求和 printSum((arr) -> { int sum = 0; for (int i : arr) { sum += i; } System.out.println("sum = " + sum); }); // 使用已有的方法进行方法引用(让已实现的方法复用) // 类名::静态方法 printSum(Demo2::getSum); } /** 已有的求和方法 **/ private static void getSum(int[] arr){ int sum = 0; for (int i : arr) { sum += i; } System.out.println("sum = " + sum); } /** 使用消费者函数式接口 **/ private static void printSum(Consumer consumer){ int[] arr = new int[]{11, 22, 33, 44}; consumer.accept(arr); } } ``` ### 4.2 方法引用格式 - 符号表示: `::` - 符号说明: 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。 - 应用场景: 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用 ### 4.3 常见引用方式 方法引用在JDK 8中使用方式相当灵活,有以下几种形式: 1. `instanceName::methodName` 对象::方法名 2. `ClassName::staticMethodName` 类名::静态方法 3. `ClassName::methodName` 类名::普通方法 4. `ClassName::new` 类名::new 调用的构造器 5. `TypeName[]::new` String[]::new 调用数组的构造器 #### 对象::成员方法 这是最常见的一种用法。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为: ```java public class MethodRefDemo { public static void main(String[] args) { Date now = new Date(); // 使用Lambda表达式获取当前时间 Supplier su1 = () -> now.getTime(); System.out.println(su1.get()); // 使用方法引用获取当前时间 Supplier su2 = now::getTime; System.out.println(su2.get()); } } ``` #### 类名::静态方法 由于在`java.lang.System`类中已经存在了静态方法 currentTimeMillis,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用, 写法是: ```java public class MethodRefDemo { public static void main(String[] args) { Supplier su3 = () -> System.currentTimeMillis(); // 等同于, 调用该类的静态方法 Supplier su4 = System::currentTimeMillis; } } ``` #### 类名::引用实例方法 Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。 ```java public class MethodRefDemo { public static void main(String[] args) { Function f1 = (str) -> str.length(); // 等同于, 将参数作为调用者去调用方法, 然后接收对应数据类型的返回值 Function f2 = String::length; BiFunction f3 = String::substring; // 等同于, 将第一个参数作为调用者, 第二个参数作为参数, 然后接收对应数据类型的返回值 BiFunction f4 = (str, index) -> str.substring(index); } } ``` #### 类名::new 由于构造器的名称与类名完全一样。所以构造器引用使用`类名称::new`的格式表示 ```java public class MethodRefDemo { public static void main(String[] args) { // 使用无参构造器实例一个String类 Supplier s1 = String::new; // 等同于 Supplier s2 = () -> new String(); s1.get(); // 把具体地调用体现在了接口上 // 使用一个参数的构造器实例一个String类 Function s3 = String::new; // 等同于 Function s4 = (str) -> new String(str); s3.apply("张三"); } } ``` #### 数组::new 数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同 ```java public class MethodRefDemo { public static void main(String[] args) { Function f = length -> new int[length]; // 等同于 Function ff = int[]::new; } } ``` #### 小结 方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式, 不过要注意的是方法引用只能"引用"已经存在的方法!