java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java中的Lambda表达式

java中的Lambda表达式使用及说明

作者:heartbeat..

文章介绍了Java 8中Lambda表达式的基本概念及其在Stream流操作中的应用,Lambda表达式简化了函数式接口的实现,与Stream结合使用更流畅,文章还提供了Lambda表达式的各种简化技巧和注意事项,如方法引用、变量捕获规则以及异常处理等

一、介绍:Stream的“黄金搭档”

 Lambda 表达式是 Java 8 引入的 函数式编程语法糖,核心作用是 简化函数式接口的实现代码(替代冗余的匿名内部类),也是 Stream 流式操作的 “黄金搭档”—— 没有 Lambda,Stream 的链式调用会变得臃肿难读。

二、先搞懂:Lambda 只为 “函数式接口” 服务

Lambda 表达式不能单独使用,它的唯一用途是 快速实现 “函数式接口”

举个对比:用匿名内部类 vs Lambda 实现 Runnable(线程任务)

// 1. 传统匿名内部类(冗余)
new Thread(new Runnable() {
    @Override
    public void run() { // 唯一抽象方法
        System.out.println("匿名内部类执行线程任务");
    }
}).start();

// 2. Lambda 表达式(简洁)
new Thread(() -> System.out.println("Lambda 执行线程任务")).start();

Lambda 自动 “对接” 函数式接口的唯一抽象方法,省去了 new 接口()@Override、方法名等冗余代码。

举个简单的例子:

假设你要给朋友寄一本书(对应 “一段核心逻辑”):

匿名内部类

就像把书放进一个完整的快递箱 —— 要选箱子(定义匿名类)、填快递单(实现接口方法)、封箱(写方法体),哪怕箱子比书还大(冗余代码比核心逻辑多),也必须按流程来。比如用 Comparator 排序:

// 匿名内部类:“完整快递箱”,冗余代码多
List<Integer> list = Arrays.asList(3,1,2);
Collections.sort(list, new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return a - b; // 核心逻辑:“书”
    }
});

Lambda 表达式

就像 “裸奔快递”—— 直接把书交给快递员,不用箱子、不用填复杂快递单,只说 “送到 XX 地址”(核心逻辑)。多余的 “包装”(类定义、方法声明)全省略,直奔主题。同样的排序逻辑,Lambda 简化后:

// Lambda:“裸奔快递”,只留核心逻辑
list.sort((a, b) -> a - b);

匿名内部类是 “裹脚布式代码”,Lambda 是 “紧身衣式代码”—— 去掉所有不必要的冗余,只保留核心逻辑,让代码从 “臃肿” 变 “精干”。

传递行为:把 “做事的方法” 当 “快递包裹” 传递(函数式接口的核心)

生活中,你可以把 “做饭” 这个行为(不是做好的饭)告诉别人:比如让妈妈 “按我的方法做饭”(传递行为),而不是自己先做好饭再交给妈妈(传递结果)。

Lambda 就是 “传递行为” 的代码版 —— 它不直接执行逻辑,而是把 “逻辑本身” 当作参数传给方法,让方法按需执行。

举例:用 Stream 过滤列表(传递 “过滤规则” 这个行为):

List<String> fruits = Arrays.asList("苹果", "香蕉", "橙子", "小番茄");
// 传递“长度>2”这个行为给 filter 方法
List<String> longFruits = fruits.stream()
                               .filter(f -> f.length() > 2) // Lambda:传递“过滤行为”
                               .collect(Collectors.toList());

普通代码是 “传递结果”(比如先过滤好列表再传入方法),Lambda 是 “传递菜谱”(告诉方法 “怎么过滤”,让方法自己执行)—— 灵活度拉满,同一个方法(如 filter)可以接收不同 “行为” 实现不同功能(过滤长度 > 2、过滤包含 “果” 字等)。

方法引用是 “快递的‘加急件’”

如果 Lambda 里的逻辑只是 “直接调用某个已有的方法”(没有额外加工),就像快递是 “直接转寄”(不用拆包、不用额外处理),这时可以用方法引用进一步简化 —— 相当于给快递贴了 “加急标签”,跳过中间冗余的 “Lambda 语法糖”,直接指向目标方法。

举例:按字符串长度排序:

// Lambda 写法:“口头指示”快递员
list.sort((s1, s2) -> s1.length() - s2.length());

// 方法引用写法:“直接写收件人”,更简洁
list.sort(Comparator.comparingInt(String::length));

Lambda 捕获的变量是 “快递单上的固定信息”—— 一旦下单(定义 Lambda),信息就不能改了,否则快递员(JVM)不知道该按哪个信息执行。

异常处理:不能 “藏着掖着” 未声明的 checked 异常

就像寄快递时不能隐瞒 “易碎品”(否则损坏不赔),Lambda 里如果抛出 checked 异常(如 IOException),必须明确处理(try-catch)或让包含它的方法声明抛出 —— 不能偷偷抛出,否则编译器会 “拒收”。

// 错误:未处理 checked 异常
List<File> files = Arrays.asList(new File("a.txt"), new File("b.txt"));
files.stream().map(file -> file.getCanonicalPath()); // getCanonicalPath 抛出 IOException

// 正确:要么 try-catch,要么声明异常
files.stream().map(file -> {
    try {
        return file.getCanonicalPath();
    } catch (IOException e) {
        throw new RuntimeException(e); // 转为 unchecked 异常
    }
});

Lambda 里的 checked 异常是 “快递里的易碎品”—— 必须提前告诉 “快递员”(编译器)怎么处理(try-catch),否则 “快递”(代码)无法正常投递(编译报错)。

三、Lambda 核心语法:(参数) -> 代码块

语法格式:(参数列表) -> { 业务代码 },有 3 个关键部分:

部分说明
(参数列表)对应函数式接口抽象方法的参数(可省略参数类型,单参数可省略括号)
->箭头运算符,分隔参数和代码块(固定写法)
{ 代码块 }对应抽象方法的实现逻辑(单条语句可省略大括号,有返回值时可省略 return)

语法简化规则(越用越简洁)

  1. 省略参数类型:Java 能通过接口自动推断参数类型;
  2. 单参数省略括号:只有 1 个参数时,(a) 可简化为 a
  3. 单语句省略大括号:代码块只有 1 行时,{ ... } 可省略;
  4. 单返回语句省略 return:若代码块是返回语句,省略大括号的同时可省略 return

语法示例(以Comparator接口为例,用于排序)

// 原始 Lambda(无简化)
Comparator<Integer> comp1 = (Integer a, Integer b) -> {
    return b - a; // 降序排序
};

// 简化1:省略参数类型(Java 自动推断)
Comparator<Integer> comp2 = (a, b) -> {
    return b - a;
};

// 简化2:单返回语句省略大括号和 return
Comparator<Integer> comp3 = (a, b) -> b - a;

// 最终简化结果(Stream 中直接使用)
List<Integer> list = Arrays.asList(3,1,2);
list.stream().sorted((a, b) -> b - a).forEach(System.out::println); // 输出 3,2,1

四、Lambda 与 Stream 的 “天作之合”

Stream 的中间操作(filtermap 等)和终端操作(forEachsorted 等),参数全是函数式接口 ——Lambda 刚好能简化这些接口的实现,让 Stream 链式调用更流畅。

Stream

常见操作

函数式接口接口抽象方法Lambda 示例(实现逻辑)
filter()Predicate<T>boolean test(T t)emp -> emp.getAge() >= 30(筛选 30 岁以上员工)
map()Function<T,R>R apply(T t)emp -> emp.getSalary()(提取员工工资)
forEach()Consumer<T>void accept(T t)System.out::println(打印元素,方法引用简化)
sorted()Comparator<T>int compare(T a,T b)(a,b) -> b - a(降序排序)
findAny()Predicate<T>boolean test(T t)n -> n % 2 == 0(找任意偶数)

示例:

List<Employee> empList = Arrays.asList(
    new Employee("张三", 28, 15000),
    new Employee("李四", 32, 18000),
    new Employee("赵六", 35, 20000)
);

// 需求:筛选30岁以上员工 → 提取姓名 → 按姓名长度降序 → 打印
empList.stream()
    .filter(emp -> emp.getAge() >= 30) // Predicate:筛选条件
    .map(Employee::getName) // Function:提取姓名(方法引用简化Lambda)
    .sorted((name1, name2) -> name2.length() - name1.length()) // Comparator:排序
    .forEach(System.out::println); // Consumer:打印

// 输出:赵六(2字)、李四(2字)(若有3字姓名会排在前面)

五、Lambda 简化技巧:方法引用(::)

当 Lambda 的代码块只是 调用一个已存在的方法(无额外逻辑),可以用 类名::方法名 或 对象::方法名 进一步简化,这就是 方法引用

方法引用类型语法格式Lambda 等效写法示例(Stream 中)
静态方法引用类名::静态方法(a,b) -> 类名.静态方法(a,b)Integer::compare → (a,b) -> Integer.compare(a,b)
实例方法引用(对象)对象::实例方法(x) -> 对象.实例方法(x)System.out::println → (s) -> System.out.println(s)
实例方法引用(参数)类名::实例方法(a,b) -> a.实例方法(b)String::equals → (s1,s2) -> s1.equals(s2)
构造器引用类名::new(参数) -> new 类名(参数)ArrayList::new → () -> new ArrayList<>()

示例:

// 1. 实例方法引用(对象):System.out::println 替代 (s) -> System.out.println(s)
List<String> names = Arrays.asList("张三", "李四");
names.stream().forEach(System.out::println);

// 2. 实例方法引用(参数):String::length 替代 (s) -> s.length()
List<Integer> nameLengths = names.stream()
    .map(String::length) // 提取姓名长度
    .collect(Collectors.toList());

// 3. 构造器引用:ArrayList::new 替代 () -> new ArrayList<>()
List<String> newNames = names.stream()
    .filter(name -> name.length() == 2)
    .collect(Collectors.toCollection(ArrayList::new));

六、Lambda 中的变量捕获

Lambda 可以访问外部变量,但有严格限制:

  1. 只能访问 final 或 effectively final 的变量(即变量声明后不再修改);
  2. 不能修改外部变量(否则编译报错)。

示例:

// 正确:外部变量是 effectively final(声明后未修改)
int minAge = 30;
empList.stream().filter(emp -> emp.getAge() >= minAge).forEach(System.out::println);

// 错误:修改外部变量(编译报错)
int count = 0;
empList.stream().forEach(emp -> {
    count++; // 报错:Variable used in lambda expression should be final or effectively final
});

原因:Lambda 本质是匿名函数,可能在多线程中执行,禁止修改外部变量是为了避免线程安全问题。

七、Lambda 常见误区

  1. 误以为能替代所有匿名内部类:只有 “函数式接口”(单抽象方法)才能用 Lambda,多抽象方法的接口不行;
  2. 修改外部变量:违反 “final 或 effectively final” 规则,编译报错;
  3. 过度简化导致可读性下降:复杂逻辑(多行业务代码)不适合用 Lambda,建议拆分为普通方法,再用方法引用调用;
  4. 忽略异常处理:Lambda 中抛出受检异常(如 IOException),需在代码块中捕获,或函数式接口的抽象方法声明该异常。

示例:

// 错误:Lambda 中抛出受检异常,未处理
List<String> files = Arrays.asList("a.txt", "b.txt");
files.stream().map(file -> new FileInputStream(file)); // 编译报错:未处理 IOException

// 正确:捕获异常
files.stream().map(file -> {
    try {
        return new FileInputStream(file);
    } catch (IOException e) {
        throw new RuntimeException(e); // 转为运行时异常抛出
    }
});

八、总结

Lambda 表达式的核心价值是 “简化代码 + 传递行为”

  1. 专为函数式接口设计,替代冗余的匿名内部类;
  2. 语法简洁灵活,配合 Stream 能大幅提升代码可读性和开发效率;
  3. 方法引用是 Lambda 的 “进阶简化技巧”,适合简单的方法调用场景;
  4. 注意变量捕获规则和异常处理,避免踩坑。

一句话记住 Lambda:“少写代码,多传逻辑”—— 它让 Java 从 “面向对象” 向 “函数式编程” 迈出了重要一步,而 Stream 则是 Lambda 最经典的应用场景。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文