Java必看!为什么你的Lambda表达式又丑又难用?这些优化技巧你必须掌握

前言

在上几篇文章中,我分享了为了进大厂背过的Java面试题:基础篇、JVM篇、Java代码精简之道,受到了很多小伙伴的关注和好评,这也让我更有动力继续为大家输出干货。
作为一名拥有 10 年 Java 开发经验的老鸟,在日常工作中,经常看到小伙伴们在使用 Lambda 表达式时遇到各种问题。
Lambda 表达式本是 Java 为了让代码更简洁、高效而生的强大工具,但很多时候却被用得“惨不忍睹”。
今天,我就来和大家深入聊聊 Lambda 表达式,从基础到优化,让你写出:最完美的Lambda表达式只有一行,轻松解决开发中的痛点。

全文近2万字,耗时一周,吐血整理!原创不易,切勿直接搬运!可转载!

1. Lambda 表达式简介

Lambda 表达式是 Java 8 引入的一个重要特性,它允许我们以更简洁的方式表示可传递给方法或存储在变量中的代码块。

简单来说,它就像是一个匿名函数,可以作为参数传递给方法,或者作为返回值从方法中返回。

想象一下,以前我们要实现一个简单的线程,需要创建一个继承自 Thread 类或者实现 Runnable 接口的类,代码繁琐。

有了 Lambda 表达式,几行代码就能搞定,极大地提高了代码的简洁性和可读性。


2. Lambda 表达式语法

2.1 无类型表达式语法

无类型表达式语法是 Lambda 表达式中最简洁的一种形式。

当参数类型可以由上下文推断出来时,我们可以省略参数的类型声明。例如:

// 定义一个加法运算的 Lambda 表达式
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
// 调用 Lambda 表达式
int result = add.apply(3, 5);
System.out.println(result); 

这里 BiFunction 是一个函数式接口,它接收两个参数并返回一个结果。
(a, b) -> a + b 就是一个无类型表达式的 Lambda 表达式,ab 的类型由 BiFunction 接口推断得出。

2.2 单参数表达式语法

当 Lambda 表达式只有一个参数时,语法更加简洁。

如果只有一个参数,括号可以省略。例如:

// 定义一个判断数字是否为偶数的 Lambda 表达式
Predicate<Integer> isEven = num -> num % 2 == 0;
// 调用 Lambda 表达式
boolean isEvenResult = isEven.test(4);
System.out.println(isEvenResult); 

这里 Predicate 是一个函数式接口,接收一个参数并返回一个布尔值。
num -> num % 2 == 0 就是单参数表达式的 Lambda 表达式。

2.3 单语句表达式语法

如果 Lambda 表达式的代码块只有一条语句,那么花括号也可以省略。例如:

// 定义一个返回字符串长度的 Lambda 表达式
Function<String, Integer> getLength = str -> str.length();
// 调用 Lambda 表达式
int length = getLength.apply("这是一个宝藏博主,希望各位老板点赞+收藏+关注哦");
System.out.println(length); 

这里 Function 是一个函数式接口,接收一个参数并返回一个结果。
str -> str.length() 就是单语句表达式的 Lambda 表达式。


3. Lambda 表达式方法引用

方法引用是一种更简洁的方式来引用已经存在的方法。它可以让代码更加清晰和易读。

3.1 构造方法引用语法

构造方法引用允许我们使用构造方法作为 Lambda 表达式的实现。例如:

// 定义一个类
class Person {
   
    private String name;
    public Person(String name) {
   
        this.name = name;
    }
    public String getName() {
   
        return name;
    }
}
// 定义一个 Supplier 接口,用于创建 Person 对象
Supplier<Person> personSupplier = Person::new;
// 调用 Supplier 接口创建 Person 对象
Person person = personSupplier.get();

这里 Person::new 就是构造方法引用,它会调用 Person 类的无参构造方法。

3.2 静态方法引用语法

静态方法引用可以引用类的静态方法。例如:

// 定义一个工具类
class MathUtils {
   
    public static int add(int a, int b) {
   
        return a + b;
    }
}
// 定义一个 BiFunction 接口,使用静态方法引用
BiFunction<Integer, Integer, Integer> addFunction = MathUtils::add;
// 调用 BiFunction 接口
int sum = addFunction.apply(2, 3);

这里 MathUtils::add 就是静态方法引用,它引用了 MathUtils 类的 add 静态方法。

3.3 实例方法引用语法

实例方法引用可以引用对象的实例方法。例如:

// 定义一个字符串对象
String str = "Hello World";
// 定义一个 Function 接口,使用实例方法引用
Function<Integer, String> substringFunction = str::substring;
// 调用 Function 接口
String subStr = substringFunction.apply(6);

这里 str::substring 就是实例方法引用,它引用了 str 对象的 substring 实例方法。

3.4 参数类方法引用语法

参数类方法引用允许我们引用参数对象的方法。例如:

// 定义一个 Consumer 接口,使用参数类方法引用
Consumer<String> printConsumer = System.out::println;
// 调用 Consumer 接口
printConsumer.accept("这是一个宝藏博主,希望各位老板点赞+收藏+关注哦");

这里 System.out::println 就是参数类方法引用,它引用了 System.out 对象的 println 方法。


4. Lambda 表达式知识点

4.1 函数式接口

函数式接口是只包含一个抽象方法的接口。

Lambda 表达式可以用来实现函数式接口。
Java 8 提供了很多内置的函数式接口,如 PredicateFunctionConsumer 等。
理解函数式接口是正确使用 Lambda 表达式的关键。

4.2 类型推断

Lambda 表达式的强大之处在于它的类型推断能力。

编译器可以根据上下文自动推断出 Lambda 表达式的参数类型和返回类型。
这使得代码更加简洁,减少了冗余的类型声明。

4.3 this 指向对象

在 Lambda 表达式中,this 关键字的指向与外层代码相同。

这与匿名内部类中 this 的指向有所不同,需要特别注意。

4.4 变量作用域

Lambda 表达式可以访问外层的局部变量,但这些变量必须是 final 或者事实上的 final(即声明后不再修改)。

这是为了保证线程安全。


5. 丑陋的Lambda表达式

5.1 无法理解的Lambda表达式

在实际项目中,我见过一些同事写出这样的Lambda表达式:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
   .map(name -> {
   
        if (name.length() > 3) {
   
            return name.toUpperCase();
        } else {
   
            return name.toLowerCase();
        }
    })
   .forEach(System.out::println);

这段代码乍一看,逻辑并不复杂,但多层嵌套和不简洁的写法,使得代码的可读性大打折扣
特别是对于新接手项目的开发人员来说,理解起来比较费劲。
就好比走进了一个迷宫,很难快速找到出口。

5.2 无法复用的Lambda表达式

有些Lambda表达式是为了一次性使用而编写的,例如:

Runnable task = () -> System.out.println("This is a one - time task");
new Thread(task).start();

虽然功能实现了,但如果在其他地方也需要执行类似的任务,就无法复用这段代码
这就像我们每次都要重新制造一个工具来完成相同的工作,浪费了很多时间和精力。

5.3 无法单测的Lambda表达式

看下面这段代码:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
   .filter(num -> {
   
        // 一些复杂且难以测试的逻辑
        if (num % 2 == 0 && num > 2) {
   
            return true;
        }
        return false;
    })
   .mapToInt(Integer::intValue)
   .sum();

由于Lambda表达式中的逻辑直接写在流式操作中,很难对其中的过滤逻辑进行单独的单元测试。这就好比一个黑盒子,我们不知道里面的具体逻辑是否正确,一旦出现问题,排查起来非常困难。

5.4 无法确定入参的Lambda表达式

在某些情况下,Lambda表达式的入参含义不明确:

Function<String, Integer> func = str -> {
   
    // 这里的逻辑依赖于外部未明确说明的规则
    return str.length() * someExternalFactor; 
};

这里的 someExternalFactor 没有明确说明来源,使得调用者很难理解这个Lambda表达式的具体功能,增加了代码维护的难度。

5.5 代码量大的Lambda表达式

当Lambda表达式中的代码块包含大量代码时:

Consumer<List<String>> processList = list -> {
   
    for (String item : list) {
   
        // 复杂的业务逻辑,可能包含多个方法调用和条件判断
        if (item.contains("keyword")) {
   
            // 一系列操作
            System.out.println(item);
            // 更多操作
        }
    }
};

代码的可读性和可维护性急剧下降,违背了Lambda表达式简洁高效的初衷。

5.6 套娃一样的Lambda表达式

多层嵌套的Lambda表达式:

Function<Integer, Function<Integer, Function<Integer, Integer>>> nestedFunc = a -> b -> c -> a + b + c;
int result = nestedFunc.apply(1).apply(2).apply

你可能感兴趣的:(Java代码优化之道,java,开发语言,后端,代码规范,面试,极限编程,软件工程)