写点什么

☕️【Java 技术之旅】带你看透 Lambda 表达式的本质

发布于: 2021 年 05 月 17 日
☕️【Java技术之旅】带你看透Lambda表达式的本质

📚 Lambda 出现之前

📚 Anonymous 匿名类

线程去完成任务时,会通过 Runnable 接口来定义任务内容,并使用 Thread 类来启动该线程。 创建 Runnable 接口的匿名内部类对象来指定线程要执行的任务内容,再将其交给一个线程来启动!传统写法,代码如下:

public class Demo01LambdaIntro {    public static void main(String[] args) {        new Thread(new Runnable() {            @Override            public void run() {                System.out.println("新线程任务执行!");            }        }).start();    }}
复制代码
📚 分析

对于 Runnable 的匿名内部类用法,可以分析出几点内容:

  • Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心;

  • 为了指定 run 的方法体,不得不需要 Runnable 接口的实现类;

  • 为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类;

  • 必须覆盖重写抽象 run 方法,方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;

  • 而实际上,似乎只有方法体才是关键所在;

📚 Lambda 表达式

借助 Java 8 的全新语法,上述 Runnable 接口的匿名内部类写法可以通过更简单的 Lambda 表达式达到相同的效果 Lambda 表达式写法,代码如下:

public class Demo01LambdaIntro {    public static void main(String[] args) {        new Thread(() -> System.out.println("新线程任务执行!")).start(); // 启动线程    }}
复制代码
📚 优点

简化匿名内部类的使用,语法更加简单;

📚 Lambda 标准格式

Lambda 的标准格式格式由 3 个部分组成:


(参数类型 参数名称) -> {

代码体;

}

📚 格式说明
  • (参数类型 参数名称):参数列表;

  • {代码体;}:方法体;

  • -> :箭头,分隔参数列表和方法体;

📚 无参数无返回值 Lambda
public class Test {      public static void main(String[] args) throws Exception {    		goSwimming(new Swimmable() {            @Override            public void swimming() {                System.out.println("匿名内部类游泳");            }        });        goSwimming(()->{ System.out.println("Lambda表达式游泳"); });    }    public static void goSwimming(Swimmable swimmable) {        swimmable.swimming();    }}interface Swimmable {    public abstract void swimming();}

//控制台打印匿名内部类游泳Lambda表达式游泳
复制代码
📚 有参数有返回值 Lambda
public class Test {    public static void main(String[] args) throws Exception {        goSwimming(new Swimmable() {            @Override            public String swimming(String str) {                return str.toUpperCase();            }        });        goSwimming((str)->{ return str.toLowerCase(); });    }    public static void goSwimming(Swimmable swimmable) {        String str = "Hello World";        str = swimmable.swimming(str);        System.out.println(str);    }}interface Swimmable {    public abstract String swimming(String str);}//控制台打印HELLO WORLDhello world
复制代码

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

📚 Lambda 实现原理

还是看前面 Test 类的例子,我们可以看到匿名内部类会在编译后产生一个类:Test$1.class

反编译 Test$1.class 代码为

final class Test$1 implements Swimmable{  public String swimming(String str){    return str.toUpperCase();  }}
复制代码

我们再来看看 Lambda 的效果,修改 Test.java 去除匿名内部类的调用:

public class Test {      public static void main(String[] args) throws Exception {        goSwimming((str)->{ return str.toLowerCase(); });    }    public static void goSwimming(Swimmable swimmable) {        String str = "Hello World";        str = swimmable.swimming(str);        System.out.println(str);    }}interface Swimmable {    public abstract String swimming(String str);}
复制代码

运行程序,控制台可以得到预期的结果,但是并没有出现一个新的类,也就是说 Lambda 并没有在编译的时候产生一个新的类,进行反编译报错。

我们使用 JDK 自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令:

javap -c -p 文件名.class-c:表示对代码进行反汇编-p:显示所有类和成员
复制代码


PS F:\CloneTest\out\production\CloneTest> javap -c -p Test.classCompiled from "Test.java"public class Test {  public Test();    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V       4: return
public static void main(java.lang.String[]) throws java.lang.Exception; Code: 0: invokedynamic #2, 0 // InvokeDynamic #0:swimming:()LSwimmable; 5: invokestatic #3 // Method goSwimming:(LSwimmable;)V 8: return
public static void goSwimming(Swimmable); Code: 0: ldc #4 // String Hello World 2: astore_1 3: aload_0 4: aload_1 5: invokeinterface #5, 2 // InterfaceMethod Swimmable.swimming:(Ljava/lang/String;)Ljava/lang/String; 10: astore_1 11: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 14: aload_1 15: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 18: return private static java.lang.String lambda$main$0(java.lang.String); Code: 0: aload_0 1: invokevirtual #8 // Method java/lang/String.toLowerCase:()Ljava/lang/String; 4: areturn}PS F:\CloneTest\out\production\CloneTest>
复制代码

看到在类中多出了一个私有的静态方法 lambda$main$0 。这个方法里面放的是什么内容呢?通过断点调试来看看:

可以确认 lambda$main$0 里面放的就是 Lambda 中的内容;

lambda$main0 的命名 : 以 lambda 开头 , 因为是在 main()函数里使用了 lambda 表达式 , 所以带有 0 的命名:以 lambda 开头,因为是在 main()函数里使用了 lambda 表达式,所以带有 0 的命名:以 lambda 开头,因为是在 main()函数里使用了 lambda 表达式,所以带有 main 表示,因为是第一个,所以 $0


添加 JVM 参数:-Djdk.internal.lambda.dumpProxyClasses 进行输出相关的 Lambda 表达式。

加上这个参数后,运行时会将生成的内部类 class 码输出到一个文

import java.lang.invoke.LambdaForm.Hidden;// 形成一个匿名内部类,实现接口,重写抽象方法// $FF: synthetic classfinal class Test$$Lambda$1 implements Swimmable {    private Test$$Lambda$1() {}    @Hidden    public String swimming(String var1) {   		//接口的重写方法中会调用新生成的方法        return Test.lambda$main$0(var1);    }}
复制代码

可以看到这个匿名内部类实现了 Swimmable 接口,并且重写了 swimming 方法, swimming 方法调用 return Test.lambda$main$0(var1),也就是调用 Lambda 中的内容。最后可以将 Lambda 理解为:

public class Test {    public static void main(String[] args) throws Exception {        goSwimming(new Swimmable() {            @Override            public String swimming(String str) {                return lambda$main$0(str);                   //对应前面debug到这,是执行lambda$main$0方法            }        });    }	//新增一个方法,这个方法的方法体就是Lambda表达式中的代码    private static String lambda$main$0(String str) {        return str.toLowerCase();    }      public static void goSwimming(Swimmable swimmable) {        String str = "Hello World";        str = swimmable.swimming(str);        System.out.println(str);    }}interface Swimmable {    public abstract String swimming(String str);}
复制代码
📚 匿名类和 Lambda 的区别

匿名内部类在编译的时候会一个 class 文件,Lambda 在程序运行的时候形成一个类:

  • 在类中新增一个静态方法,这个方法的方法体就是 Lambda 表达式中的代码;

  • 还会形成一个匿名内部类,实现接口,重写抽象方法;

  • 在接口的重写方法中会调用新生成的方法;

📚 Lambda 省略格式

在 Lambda 标准格式的基础上,使用省略写法的规则为:

  • 小括号内参数的类型可以省略;

  • 如果小括号内有且仅有一个参数,则小括号可以省略;

  • 如果大括号内有且仅有一个语句,可以同时省略大括号、return 关键字及语句分号;

(int a) -> {return new Person();}
复制代码

📚 Lambda 的前提条件

Lambda 的语法非常简洁,但是 Lambda 表达式不是随便使用的,使用时有几个条件要特别注意:

  • 方法的参数或局部变量类型必须为接口才能使用 Lambda;

  • 接口中有且仅有一个抽象方法;(属于函数接口)

public class Test {    public static void main(String[] args) throws Exception {        //方法的参数是接口:Lambda表达式,省略参数类型以及{}、return、;        goSwimming(str -> str.toUpperCase());        //局部变量类型为接口:匿名内部类形式        Swimmable swimmable = new Swimmable() {            @Override            public String swimming(String str) {                return null;            }        };        //局部变量类型为接口:Lambda表达式        Swimmable swimmable2 = str -> str.toUpperCase();    }    public static void goSwimming(Swimmable swimmable) {        String str = "Hello World";        str = swimmable.swimming(str);        System.out.println(str);    }}//定义一个接口,有且仅有一个抽象方法interface Swimmable {    public abstract String swimming(String str);}
复制代码

📚 函数式接口

函数式接口在 Java 中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。Java 中的函数式编程体现就是 Lambda,所以函数式接口就是可以适

用于 Lambda 使用的接口。只有确保接口中有且仅有一个抽象方法,Java 中的 Lambda 才能顺利地进行推导

📚 FunctionalInterface 注解

与 @Override 注解的作用类似,Java 8 中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。该注解可用于一个接口的定义上:

@FunctionalInterfacepublic interface Operator {	void myMethod();}
复制代码

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样;

📚 常用内置函数式接口

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

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

📚 Supplier 接口
@FunctionalInterfacepublic interface Supplier<T> {	public abstract T get();}
复制代码

java.util.function.Supplier 接口,意味着“供给” , 对应的 Lambda 表达式需要“对外提供”符合泛型类型的对象数据;

供给型接口,通过 Supplier 接口中的 get 方法可以得到一个值,无参有返回的接口;

使用场景演示:使用 Lambda 表达式返回数组元素最大值!

import java.util.Arrays;import java.util.function.Supplier;
public class Test { public static void main(String[] args) throws Exception { printMax(() -> { int[] arr = {10, 20, 100, 30, 40, 50}; // 先排序,最后就是最大的 Arrays.sort(arr); return arr[arr.length - 1]; // 最后就是最大的 }); } private static void printMax(Supplier<Integer> supplier) { int max = supplier.get(); System.out.println("max = " + max); }}//控制台打印:max = 100
复制代码
📚 Consumer 接口:
@FunctionalInterfacepublic interface Consumer<T> {	public abstract void accept(T t);}
复制代码

java.util.function.Consumer 接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定;使用场景演示:使用 Lambda 表达式将一个字符串转成大写和小写的字符串

import java.util.function.Consumer;public class Test {    public static void main(String[] args) {        // Lambda表达式        test((String s) -> {            System.out.println(s.toLowerCase());        });    }    public static void test(Consumer<String> consumer) {        consumer.accept("HelloWorld");    }}//控制台打印:helloworld
复制代码


  • ✒️ 默认方法:andThen

如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的 default 方法 andThen;

要想实现组合,需要两个或多个 Lambda 表达式即可,而 andThen 的语义正是“一步接一步”操作。例如两个步骤组合的情况:

default Consumer<T> andThen(Consumer<? super T> after) {		Objects.requireNonNull(after);			return (T t) -> {       	accept(t);         after.accept(t);     };}
复制代码

例如两个步骤组合的情况:

import java.util.function.Consumer;
public class Test { public static void main(String[] args) { // Lambda表达式 test((String s) -> { System.out.println(s.toLowerCase()); }, (String s) -> { System.out.println(s.toUpperCase()); }); // Lambda表达式简写 /*test(s -> System.out.println(s.toLowerCase()), s -> System.out.println(s.toUpperCase()));*/ } public static void test(Consumer<String> c1, Consumer<String > c2) { String str = "Hello World"; //先执行c1(转为小写)再执行c2(转为大写) c1.andThen(c2).accept(str); //先执行c2(转为大写)再执行c1(转为小写) c2.andThen(c1).accept(str); }}//控制台输出:hello worldHELLO WORLDHELLO WORLDhello world
复制代码
📚 Function 接口:
@FunctionalInterfacepublic interface Function<T, R> {	public abstract R apply(T t);}
复制代码

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

import java.util.function.Function;
public class Test { // public static void main(String[] args) { // Lambda表达式 test((String s) -> { return Integer.parseInt(s); // 10 }); } public static void test(Function<String, Integer> function) { Integer in = function.apply("10"); System.out.println("in: " + (in + 5)); }}//控制台输出:in: 15
复制代码


  • 默认方法:andThen

Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK 源代码如:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {		Objects.requireNonNull(after);		return (T t) -> after.apply(apply(t));}
复制代码

该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多:

import java.util.function.Function;
public class Test { public static void main(String[] args) { // Lambda表达式 test((String s) -> { return Integer.parseInt(s); }, (Integer i) -> { return i * 10; }); } public static void test(Function<String, Integer> f1, Function<Integer, Integer> f2) { // Integer in = f1.apply("66"); // 将字符串解析成为int数字 // Integer in2 = f2.apply(in);// 将上一步的int数字乘以10 Integer in3 = f1.andThen(f2).apply("66"); System.out.println("in3: " + in3); // 660 }}//控制台输出:in3: 660
复制代码

第一个操作是将字符串解析成为 int 数字,第二个操作是乘以 10。两个操作通过 andThen 按照前后顺序组合到了一起;

📚 Predicate 接口

Predicate 接口用于做判断,返回 boolean 类型的值

@FunctionalInterfacepublic interface Predicate<T> {   public abstract boolean test(T t);}
复制代码

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

例如:使用 Lambda 判断一个人名如果超过 3 个字就认为是很长的名字

import java.util.function.Predicate;
public class Test { public static void main(String[] args) { test(s -> s.length() > 3, "迪丽热巴"); } private static void test(Predicate<String> predicate, String str) { boolean veryLong = predicate.test(str); System.out.println("名字很长吗:" + veryLong); }
}//控制台输出:名字很长吗:true
复制代码

Predicate 拥有三个默认方法:

  • 默认方法:and,将两个 Predicate 条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用 default 方法 and 。

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

  • 默认方法:negate,“与”、“或”已经了解了,剩下的“非”(取反)也会简单。默认方法 negate 的 JDK 源代码为:

📚 方法引用
import java.util.function.Consumer;
public class Test { //传统定义一个方法,直接调用即可 public static void getMax(int[] arr) { int sum = 0; for (int n : arr) { sum += n; } System.out.println(sum); } public static void main(String[] args) { //Lambda表达式实现,需要定义一个方法,然后lambda里面还要再实现一次。对比一下,lambda有点冗余了 printMax((int[] arr) -> { int sum = 0; for (int n : arr) { sum += n; } System.out.println(sum); }); } private static void printMax(Consumer<int[]> consumer) { int[] arr = {10, 20, 30, 40, 50}; consumer.accept(arr); }}//控制台输出:150
复制代码

如果我们在 Lambda 中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接“引用”过去就好了:

import java.util.function.Consumer;
public class Test { public static void getMax(int[] arr) { int sum = 0; for (int n : arr) { sum += n; } System.out.println(sum); } public static void main(String[] args) { printMax(Test::getMax); } private static void printMax(Consumer<int[]> consumer) { int[] arr = {10, 20, 30, 40, 50}; consumer.accept(arr); }}//控制台输出:150
复制代码

请注意其中的双冒号 :: 写法,这被称为“方法引用”,是一种新的语法。

方法引用的格式:

  • 符号表示 : ::

  • 符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用;

  • 如果 Lambda 所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用;

常见引用方式:

方法引用在 JDK 8 中使用方式相当灵活,有以下几种形式:

  • instanceName::methodName 对象::方法名;

  • ClassName::staticMethodName 类名::静态方法;

  • ClassName::methodName 类名::普通方法;

  • ClassName::new 类名::new 调用的构造器;

  • TypeName[]::new String[]::new 调用数组的构造器;

对象名::引用成员方法:

如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为

import java.util.Date;import java.util.function.Supplier;
public class Test { public static void main(String[] args) { Date now = new Date(); //局部变量类型为函数式接口时可以使用lambda Supplier<Long> supp = () -> { return now.getTime(); }; System.out.println(supp.get()); Supplier<Long> supp2 = now::getTime; System.out.println(supp2.get()); }}//控制台输出:15876949860551587694986055
复制代码

方法引用的注意事项:

  1. 被引用的方法,参数要和接口中抽象方法的参数一样;

  2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值;

比如:

类名::引用静态方法

import java.util.function.Supplier;
public class Test { public static void main(String[] args) { Supplier<Long> supp = () -> { return System.currentTimeMillis(); }; System.out.println(supp.get()); Supplier<Long> supp2 = System::currentTimeMillis; System.out.println(supp2.get()); }}//控制台输出:15876952237901587695223809//public static native long currentTimeMillis();
复制代码

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

类名::引用实例方法

import java.util.function.Function;
public class Test { public static void main(String[] args) { //Lambda表达式 Function<String, Integer> f1 = (s) -> { return s.length(); }; System.out.println(f1.apply("abc")); //方法引用 Function<String, Integer> f2 = String::length; System.out.println(f2.apply("abc")); }}//控制台打印:33//public int length() { return value.length;}
复制代码

类名::new 引用构造器

public class Person {    private String name;    private Integer age;
public Person(String name, Integer age) { this.name = name; this.age = age; }
public Person() { }}
复制代码


import java.util.function.BiFunction;import java.util.function.Supplier;
public class Test { public static void main(String[] args) { //Lambda表达式 Supplier<Person> sup = () -> { return new Person(); }; System.out.println(sup.get()); //方法引用 Supplier<Person> sup2 = Person::new; System.out.println(sup2.get()); //带入参的方法引用,BiFunction(String, Integer为方法入参,Person为出参) BiFunction<String, Integer, Person> fun2 = Person::new; System.out.println(fun2.apply("张三", 18)); }}//控制台打印:Person@3d075dc0Person@7cca494bPerson@58372a00
复制代码

数组::new 引用数组构造器

import java.util.function.Function;
public class Test { public static void main(String[] args) { //Lambda表达式 Function<Integer, String[]> fun = (len) -> { return new String[len]; }; String[] arr1 = fun.apply(10); System.out.println(arr1 + ", " + arr1.length);
//方法引用 Function<Integer, String[]> fun2 = String[]::new; String[] arr2 = fun2.apply(5); System.out.println(arr2 + ", " + arr2.length); }}//控制台打印:[Ljava.lang.String;@3d075dc0, 10[Ljava.lang.String;@7cca494b, 5
复制代码

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

📚 Lambda 和匿名内部类对比

  1. 所需的类型不一样:匿名内部类,需要的类型可以是类,抽象类,接口,Lambda 表达式,需要的类型必须是接口;

  2. 抽象方法数量不一样:匿名内部类所需接口中抽象方法的数量随意,Lambda 表达式所需的接口只能有一个抽象方法;

  3. 实现原理不同:匿名内部类是在编译后会形成 class,Lambda 表达式是在程序运行的时候动态生成 class;


用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
☕️【Java技术之旅】带你看透Lambda表达式的本质