JDK8 特性(一)

2022/5/23 JDK8

# 1 Lambda表达式

# 1.1 Lambda标准格式

  • Lambda省去面向对象的条条框框, Lambda的标准格式格式由3个部分组成
  • 格式说明:
    • (...args): 参数列表
    • ->: 分隔或连接参数与方法体的标识符
    • {...;}: 方法体, 主要的代码逻辑
  • eg:
class Demo{
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("线程启动");
        }).start();
    }
}
1
2
3
4
5
6
7

# 1.2 Lambda初体验

# 无参无返回值

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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 有参有返回值

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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 小结

以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写

# 1.3 Lambda实现原理

现有以下类

  • 接口类

    public interface Swimable {
        void swimming();
    }
    
    1
    2
    3
  • main入口

    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();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  • 将以上的main方法进行执行后, 会编译生成以下字节码文件

  • 将内部类的字节码文件通过 [XJad] 进行反编译

    • 匿名内部类在编译后会形成一个新的类.$
    // 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(){}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
  • main入口改为使用Lambda表达式

    public class SwimImplDemo {
        public static void main(String[] args) {
            goSwimming(() -> {
                System.out.println("去Lambda游泳了");
            });
        }
    
        private static void goSwimming(Swimable swim){
            swim.swimming();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 将以上的main方法进行执行后, 不会生成多余的字节码文件

    • 使用 [XJad] 反编译工具会失败
    • 使用JDK工具来对Lambda表达式的字节码进行反汇编
    javap -c -p 文件名.class 
    -c:表示对代码进行反汇编 -p:显示所有类和成员 
    
    1
    2
  • 对Lambda表达式的字节码文件进行反汇编

    public class com.zhuhjay.lambda.SwimImplDemo {
      public com.zhuhjay.lambda.SwimImplDemo();
        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/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
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    • 通过与源代码的比较后, 发现多了静态方法lambda$main$0

    • 得出: Lambda表达式会在类中新生成一个私有的静态方法, 命名为lambda$方法名$序列

    • 通过断点调试Lambda也可以从调用栈中发现该静态方法的生成(当然不会显式的在源码中出现)

    • 验证在Lambda表达式运行中会生成一个内部类

      • 命令格式: java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名

      • 执行(需要退出包运行该命令): java -Djdk.internal.lambda.dumpProxyClasses com.zhuhjay.lambda.SwimImplDemo

        • 查看反编译结果
        final class SwimImplDemo$$Lambda$1
                 implements Swimable {
        
            public void swimming() {
                   SwimImplDemo.lambda$main$0();
            }
        
            private SwimImplDemo$$Lambda$1() {}
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
  • 对以上结论来推断Lambda生成的字节码文件

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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 小结
    • 匿名内部类在编译的时候会一个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表达式,需要的类型必须是接口
  1. 抽象方法的数量不一样
  • 匿名内部类所需的接口中抽象方法的数量随意
  • Lambda表达式所需的接口只能有一个抽象方法
  1. 实现原理不同
  • 匿名内部类是在编译后会形成class
  • Lambda表达式是在程序运行的时候动态生成class

# 2 JDK8接口增强

# 2.1 增强介绍

  • JDK8以前的接口:

    interface 接口名 {  
        静态常量;  
        抽象方法; 
    } 
    
    1
    2
    3
    4
  • JDK8的接口:

    interface 接口名 {  
        静态常量;  
        抽象方法;  
        默认方法;  
        静态方法; 
    }
    
    1
    2
    3
    4
    5
    6

# 2.2 接口默认方法和静态方法的区别

  1. 默认方法通过实例调用,静态方法通过接口名调用。
  2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
  3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。

# 3 常用内置函数式接口

# 3.1 内置函数式接口的由来

我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。

# 3.2 常用函数式接口的介绍

它们主要在java.util.function包中。下面是最常用的几个接口。

# Supplier:生产者

java.util.function.Supplier<T>接口,它意味着"供给", 对应的Lambda表达式需要"对外提供"一个符合泛型类型的对象数据。

@FunctionalInterface 
public interface Supplier<T> {     
    public abstract T get(); 
}
1
2
3
4

# Consumer:消费者

java.util.function.Consumer<T>接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。

@FunctionalInterface 
public interface Consumer<T> {     
    public abstract void accept(T t);
    /** 该方法使得两个Consumer先后调用 c1.andThen(c2).accept(t) **/
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}
1
2
3
4
5
6
7
8
9
  • 默认方法: andThen
    • 如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen
    • 要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是"一步接一步"操作

# Function:类型转换

java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。

@FunctionalInterface
public interface Function<T, R> {  
    public abstract R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
}
1
2
3
4
5
6
7
8
9
  • 默认方法: andThen
    • Function 接口中有一个默认的 andThen 方法,用来进行组合操作
    • 该方法同样用于"先做什么,再做什么"的场景,和 Consumer 中的 andThen 差不多

# Predicate:判断

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate<T>接口。

@FunctionalInterface 
public interface Predicate<T> {
    public abstract boolean test(T t);
    /** && **/
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
    /** ! **/
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }
    /** || **/
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • 默认方法: and

    • 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用"与"逻辑连接起来实现"并且"的效果时,可以使用default方法 and
  • 默认方法: or

    • 与 and 的"与"类似,默认方法 or 实现逻辑关系中的"或"
  • 默认方法: negate

    • "与"、"或"已经了解了,剩下的"非"(取反)也会简单。它是执行了test方法之后,对结果boolean值进行"!"取反而已。一定要在 test 方法调用之前调用 negate 方法

# 4 方法引用

方法引用的注意事项

  1. 被引用的方法,参数要和接口中抽象方法的参数一样
  2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值

# 4.1 方法引用简化Lambda

使用Lambda表达式求一个数组的和

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<int[]> consumer){
        int[] arr = new int[]{11, 22, 33, 44};
        consumer.accept(arr);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 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 调用数组的构造器

# 对象::成员方法

这是最常见的一种用法。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:

public class MethodRefDemo {
    public static void main(String[] args) {
        Date now = new Date();
        // 使用Lambda表达式获取当前时间
        Supplier<Long> su1 = () -> now.getTime();
        System.out.println(su1.get());
        // 使用方法引用获取当前时间
        Supplier<Long> su2 = now::getTime;
        System.out.println(su2.get());
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 类名::静态方法

由于在java.lang.System类中已经存在了静态方法 currentTimeMillis,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用, 写法是:

public class MethodRefDemo {
    public static void main(String[] args) {
        Supplier<Long> su3 = () -> System.currentTimeMillis();
        // 等同于, 调用该类的静态方法
        Supplier<Long> su4 = System::currentTimeMillis;
    }
}
1
2
3
4
5
6
7

# 类名::引用实例方法

Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。

public class MethodRefDemo {
    public static void main(String[] args) {
        Function<String, Integer> f1 = (str) -> str.length();
        // 等同于, 将参数作为调用者去调用方法, 然后接收对应数据类型的返回值
        Function<String, Integer> f2 = String::length;

        BiFunction<String, Integer, String> f3 = String::substring;
        // 等同于, 将第一个参数作为调用者, 第二个参数作为参数, 然后接收对应数据类型的返回值
        BiFunction<String, Integer, String> f4 = (str, index) -> str.substring(index);
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 类名::new

由于构造器的名称与类名完全一样。所以构造器引用使用类名称::new的格式表示

public class MethodRefDemo {
    public static void main(String[] args) {
        // 使用无参构造器实例一个String类
        Supplier<String> s1 = String::new;
        // 等同于
        Supplier<String> s2 = () -> new String();
        s1.get();

        // 把具体地调用体现在了接口上
        
        // 使用一个参数的构造器实例一个String类
        Function<String, String> s3 = String::new;
        // 等同于
        Function<String, String> s4 = (str) -> new String(str);
        s3.apply("张三");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 数组::new

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同

public class MethodRefDemo {
    public static void main(String[] args) {
        Function<Integer, int[]> f = length -> new int[length];
        // 等同于
        Function<Integer, int[]> ff = int[]::new;
    }
}
1
2
3
4
5
6
7

# 小结

方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式, 不过要注意的是方法引用只能"引用"已经存在的方法!