一文读懂 Lambda

Lambda的设计是为了代码的优雅,要想理解Lambda,得从很开始讲起。

下面是一句话理解部分,如果你看到这一句话后会了,那后面都长篇大论就不用看了。

lambda作用是减少一次性实现接口/匿名内部类的冗余,是对匿名内部类的简化,本质是一个"可传递的行为(函数)。

1 从接口开始的最佳实践

我们都知道,职责划分核心思想是:任务逻辑和执行逻辑彻底解耦,接口定义能力,实现类做业务,执行器做流程控制,装饰器做业务增强增强。

当我们刚开始使用接口时,可能会这样

java 复制代码
// 文件一:任务接口
interface Task {
    void execute();
}

// 文件二:实现Task的类
class MyTask implements Task {

	// 实现类的方法
    @Override
    public void execute() {
        System.out.println("任务执行了!");
    }
	//
    public static void main(String[] args) {
        Test test = new Test();
         task.execute();
    }
}
  • 不知道你有没有意识到一件事?这段代码,不优雅,因为:
    • 没有复用性
    • 无法统一添加执行前后的逻辑
    • 控制权和任务逻辑混杂在一起

可能你暂时看不出来?那看看经过注释后的类吧

java 复制代码
class MyTask implements Task {

	// 实现类的方法
    @Override
    public void execute() {
        System.out.println("任务执行了!");
    }
	
    public static void main(String[] args) {
   		
        Test test = new Test();
        //第一次调用,我得把调用前后逻辑直接加在这里
        System.out.println("执行前的逻辑"); 
        task.execute();  // 任务逻辑
        System.out.println("执行后的逻辑"); 
		//其他代码
        // 第二次调用,又得重复前后逻辑,这就是没有复用性
        System.out.println("执行前的逻辑"); 
		task.execute();// 任务逻辑
		System.out.println("执行后的逻辑"); 
        
        // 每次调用task.execute()都需要添加执行前的逻辑和执行后的逻辑,因此,main方法即当妈,又当爸,一边控制任务什么时候执行,一边编写着任务逻辑
    }
}
  • 相信这下你该明白代码为什么不优雅了,那如何解决这个不优雅呢?你可能会想,可以这样写
java 复制代码
class MyTask implements Task {

	// 实现类的方法
    @Override
    public void execute() {
    	System.out.println("执行前的逻辑"); 
        task.execute();  // 任务逻辑
        System.out.println("执行后的逻辑"); 
    }
	
    public static void main(String[] args) {
        Test test = new Test();   
		task.execute();
        // 其他代码
		task.execute();
    }
}
  • 乍一看,是少了很多代码,但好像还有些问题?如果我有两种任务逻辑呢?
java 复制代码
   // 任务逻辑1 
   public void execute() {
    	System.out.println("执行前的逻辑"); 
        task.execute();  // 任务逻辑
        System.out.println("执行后的逻辑"); 
    }
java 复制代码
   // 任务逻辑2
   public void execute() {
    	System.out.println("执行前的逻辑"); 
        task.execute();  // 任务逻辑
    }
  • 很显然,这样写根本没有解决问题!甚至无法实现两种任务逻辑。更好的解决方法是下面这样
java 复制代码
// 文件一:任务接口
interface Task {
    void execute();
}
// 文件二:实现了这个接口的类
class MyTask implements Task {

    @Override
    public void execute() {
        System.out.println("任务执行了!");
    }

    public static void main(String[] args) {
        Test test = new Test();
        runTask(test); //现在就可以调用任意次数了
        // 其他代码
        runTask(test);
    }

    public static void runTask(Task task) {
		System.out.println("执行前的逻辑"); 
        task.execute();  // 任务逻辑
        System.out.println("执行后的逻辑"); 
    }
    public static void runTask1(Task task) {
		System.out.println("执行前的逻辑"); 
        task.execute();  // 任务逻辑
    }
}
// 如果你读过设计模式,应该会感觉到 runTask,runTask1是手动实现装饰器,且只像个增强行为,真正的装饰器不应该这么写。本文只是为了理解Lambda,而如此设计案例
  • 就如文章一开始说的一样:**任务逻辑和执行逻辑彻底解耦,接口定义能力,实现类做业务,执行器做流程控制,装饰器做业务增强增强。**这段代码,在小的地方完美了,但如果我们将目光放大到类上呢?就会注意到 class MyTask implements Task
    • MyTask 同时承担了业务逻辑和程序控制(main 方法),即它既是业务类,也是执行器。

更好的方法是:

java 复制代码
// 文件一:任务接口
interface Task {
    void execute();
}

// 文件二:业务实现类,只关注这个业务是怎么执行的
class MyTask implements Task {
    @Override
    public void execute() {
        System.out.println("任务执行了!");
    }
}

// 文件三:执行器类,只做流程控制
class TaskExecutor {
    public static void runTask(Task task) {
        System.out.println("执行前的逻辑");
        task.execute(); // 任务逻辑
        System.out.println("执行后的逻辑");
    }
}

// 文件四:程序入口,只负责调用
public class Main {
    public static void main(String[] args) {
        Task myTask = new MyTask();
        TaskExecutor.runTask(myTask);
        TaskExecutor.runTask(myTask); // 可以任意调用
    }
}
  • 现在,这段代码很完美的。但有足足四个文件!如果有YourTask业务,那还会多出一个类......或者,有的事情只需要执行一次,然后就完全不用了。这些场景,要怎么解决?

1.1 如何面对临时任务?

答案:使用内部匿名类

java 复制代码
// 文件一:任务接口
interface Task {
    void execute();
}

// 文件二:执行器类,只做流程控制
class TaskExecutor {
    public static void runTask(Task task) {
        System.out.println("执行前的逻辑");
        task.execute(); // 任务逻辑
        System.out.println("执行后的逻辑");
    }
}

// 文件三:程序入口,只负责调用
public class Main {
    public static void main(String[] args) {

        // 使用匿名内部类实现 Task
        Task myTask = new Task() {
            @Override
            public void execute() {
                System.out.println("任务执行了!");
            }
        };

        TaskExecutor.runTask(myTask);
        TaskExecutor.runTask(myTask); // 可以任意调用
    }
}
  • 通过匿名内部类,我们成功将MyTask类变成了程序入口中的几行代码,那么这些和Lambda有什么关系呢?

2 Lambda

请循其本,文章的一开始就说了:

Lambda 作用是:减少一次性实现接口/匿名内部类的冗余,是对匿名内部类的简化,本质是一个"可传递的行为(函数)。

是的,Lambda是语法糖,为了将代码继续简化,而出现的东西。使用了Lambda,原本的代码

java 复制代码
// 使用匿名内部类实现 Task
Task myTask = new Task() {
	@Override
	public void execute() {
		System.out.println("任务执行了!");
  }
};

可以变成

java 复制代码
Task myTask = () -> System.out.println("任务执行了!");
名称 代码 含义
参数列表 () Task.execute() 没有参数,所以空括号
箭头 -> 分隔符,把"参数"和"方法体"分开
方法体 System.out.println("任务执行了!") 你想执行的代码块

2.1 在内存中

名称 代码示例 / 描述 含义 / 内存作用
局部变量引用 Task myTask = ... 栈帧中的局部变量表存放引用,指向 Lambda 对象或缓存的单例 Lambda
方法体 () -> System.out.println("任务执行了!") Lambda 逻辑,编译器生成静态方法存放在方法区 / 元空间,不占堆
Lambda对象 仅当捕获外部变量或需要状态时存在 堆上对象,用来保存捕获的外部变量或状态;无状态 Lambda JVM 可复用或不分配堆
捕获变量 int x = 10; Task t = () -> System.out.println(x); 外部局部变量值被复制到 Lambda 对象中(effectively final),对象存放堆或复用缓存
invokedynamic 字节码调用指令 运行期动态生成 Lambda 实现,绑定方法体和接口,JVM 使用方法句柄执行

2.2 适用条件

2.2.3 目标类型必须是函数式接口(必须满足)

java 复制代码
@FunctionalInterface
interface Task {
    void execute();
}
  • 只能有一个抽象方法,默认方法 / static 方法不算
  • 多个抽象方法 → 不能用 Lambda

2.3 从设计角度来说

适合:

复制代码
() -> System.out.println("执行任务");

不适合:

复制代码
() -> {
    // 100 行复杂逻辑
    // 多层 if / try-catch
}

复杂逻辑使用外部类更清晰

行为 Lambda 是否允许
读取外部局部变量 允许
修改外部局部变量 不允许
在 Lambda 内声明局部变量 允许
在多次执行间保留状态 不允许
定义成员变量 不允许

2.4 使用场景

1:一次性策略 / 临时规则

复制代码
Collections.sort(list, (a, b) -> b - a);

特点:

  • 规则只用一次
  • 没有业务命名价值

2:回调 / 事件处理

复制代码
executor.submit(() -> doWork());
button.onClick(() -> save());

3:遍历与流式处理

复制代码
list.forEach(item -> System.out.println(item));
list.stream()
    .filter(x -> x > 10)
    .map(x -> x * 2)
    .forEach(System.out::println);

4:模板方法 / 执行器的"可变步骤"

复制代码
TaskExecutor.runTask(() -> System.out.println("任务执行了"));

2.5 源码

  • 相关源码在 java.lang.invoke.LambdaMetafactory,可以自己读读。

3 更简洁的语法糖:方法引用

Lambda,已经很简单了,那能不能更简单呢?答案是 : 用方法引用

有一个需求,给list做排序,需要怎么做呢?

使用匿名内部类

java 复制代码
// 使用匿名内部类

public class SortExampleAnonymous {
    public static void main(String[] args) {
        
        List<String> names = Arrays.asList("Bob", "Alice", "Charlie");

        names.sort(new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                return s1.compareTo(s2); 
            }
        });

        System.out.println(names);
    }
}

使用Lambda表达式

复制代码
public class SortExampleLambda {
    public static void main(String[] args) {
    
        List<String> names = Arrays.asList("Bob", "Alice", "Charlie");

        names.sort((s1, s2) -> s1.compareTo(s2)); 

        System.out.println(names);
    }
}

使用方法引用

复制代码
public class SortExampleMethodRef {
    public static void main(String[] args) {
    
        List<String> names = Arrays.asList("Bob", "Alice", "Charlie");

        // 方法引用
        names.sort(String::compareToIgnoreCase);  
        // 需要满足:
        // 1. 引用处需要是函数式接口
        // 2. 被引用的方法需要已经存在
        // 3. 被引用方法的形参和返回值需要跟抽象方法的形参和返回值保持一致
        // 4. 被引用方法的功能需要满足当前的要求

        System.out.println(names);
    }
}
  • String::compareToIgnoreCase的意思是使用Stringi类下的compareToIgnoreCase,方法引用写在括号里的方法一定要是已有的方法。就如下面这样

    public class SortExampleCustomMethod {
    public static void main(String[] args) {
    List<String> names = Arrays.asList("Bob", "Alice", "Charlie");

    复制代码
          // 使用自定义方法进行排序
          names.sort(SortExampleCustomMethod::compareIgnoreCase);
    
          System.out.println(names);
      }
    
      // 自定义比较方法
      public static int compareIgnoreCase(String s1, String s2) {
          return s1.toLowerCase().compareTo(s2.toLowerCase());
      }

    }

  • 方法引用对JavaAPI了解要求比较高

形式 语法示例 条件
静态方法引用 ClassName::staticMethod 参数顺序和类型要与函数式接口的抽象方法匹配
实例方法引用(特定对象) instance::instanceMethod 接口方法的参数列表要与实例方法匹配(不包含对象本身)
类名引用实例方法 ClassName::instanceMethod 接口方法的第一个参数作为调用对象,其余参数作为方法参数
构造器引用 ClassName::new 接口方法的参数列表要与构造函数参数匹配,返回类型是对象类型
  • 方法引用可以用在 函数式接口 (Functional Interface)的上下文中,也就是说接口中必须 只有一个抽象方法

  • 方法引用不能修改方法签名,它只是 Lambda 的简写。

  • 方法引用比 Lambda 简洁,但不适合复杂逻辑。

  • 参数类型可以通过上下文推断,所以通常不需要显示写出。

  • lambda在我眼里算能接受的而方法引用抽象的太深了,也不是本文重点,不多介绍。

相关推荐
yangminlei6 小时前
使用 Cursor 快速创建一个springboot项目
spring boot·ai编程
学到头秃的suhian6 小时前
Java的锁机制
java
Amarantine、沐风倩✨7 小时前
一次线上性能事故的处理复盘:从 SQL 到扩容的工程化思路
java·数据库·sql·oracle
tb_first7 小时前
万字超详细苍穹外卖学习笔记1
java·jvm·spring boot·笔记·学习·tomcat·mybatis
代码匠心7 小时前
从零开始学Flink:状态管理与容错机制
java·大数据·后端·flink·大数据处理
分享牛7 小时前
LangChain4j从入门到精通-11-结构化输出
后端·python·flask
zhougl9967 小时前
Java内部类详解
java·开发语言
茶本无香7 小时前
设计模式之十二:模板方法模式Spring应用与Java示例详解
java·设计模式·模板方法模式
灯火不休ᝰ7 小时前
[kotlin] 从Java到Kotlin:掌握基础语法差异的跃迁指南
java·kotlin·安卓
KoiHeng8 小时前
Java的文件知识与IO操作
java·开发语言