函数式编程与 Stream API 学习笔记

本文整理 Java 8 中的 Lambda 表达式、方法引用、函数式接口、闭包、柯里化、高阶函数和 Stream API。


1. 纯函数

纯函数通常具备以下特点:

  1. 相同的输入始终得到相同的输出
  2. 不修改函数外部的状态。
  3. 如果读取外部数据,外部数据在执行期间必须保持不变。
  4. 不产生文件写入、数据库修改、网络请求等副作用。

示例:

public int add(int a, int b) {
    return a + b;
}

上面的 add 方法只依赖参数,不修改外部状态,因此可以视为纯函数。


2. 函数对象化的好处

Java 8 可以通过函数式接口,把一段行为作为对象进行传递。

2.1 行为参数化

方法中的某个行为可以由调用方传入。

public static <T> List<T> filter(
        List<T> list,
        Predicate<T> predicate) {

    List<T> result = new ArrayList<T>();

    for (T item : list) {
        if (predicate.test(item)) {
            result.add(item);
        }
    }

    return result;
}

调用时传入不同的判断逻辑:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

List<Integer> evenNumbers = filter(
        numbers,
        number -> number % 2 == 0
);

2.2 延迟执行

函数对象本身可以先被传递,等满足条件时再执行。

logger.debug("{}", expensive());

上面的写法会先执行 expensive(),即使当前日志级别不是 DEBUG,也可能产生不必要的开销。

如果日志框架支持 Supplier 风格的延迟参数,可以写成:

logger.debug("{}", () -> expensive());

注意:是否支持这种写法取决于具体日志框架和版本,并不是所有 logger.debug 都支持 Lambda 参数。


3. Lambda 表达式

3.1 基本格式

(参数列表) -> 表达式

或者:

(参数列表) -> {
    多行代码;
    return 返回值;
}

3.2 常见写法

明确声明参数类型:

(int a, int b) -> a + b

由编译器推断参数类型:

(a, b) -> a + b

只有一个参数时,可以省略括号:

a -> a + 1

无参数时,括号不能省略:

() -> System.out.println("执行任务")

多行代码必须使用大括号:

(int a, int b) -> {
    int result = a + b;
    return result;
}

3.3 常见错误

错误写法:

a -> return a;

正确写法:

a -> a

或者:

a -> {
    return a;
}

4. 方法引用

方法引用是 Lambda 表达式的简化写法。

基本格式:

类名或对象名::方法名

4.1 静态方法引用

Lambda:

(String value) -> Integer.parseInt(value)

方法引用:

Integer::parseInt

完整示例:

Function<String, Integer> parser = Integer::parseInt;

Integer number = parser.apply("123");

4.2 指定对象的实例方法引用

String prefix = "Java";

Supplier<Integer> lengthSupplier = prefix::length;

等价于:

Supplier<Integer> lengthSupplier = () -> prefix.length();

4.3 任意对象的实例方法引用

Lambda:

(Student student) -> student.getName()

方法引用:

Student::getName

这里的 Student 不是静态调用。函数式接口传入的第一个参数会作为方法调用者。

Function<Student, String> getName = Student::getName;

4.4 构造方法引用

无参构造:

Supplier<Student> supplier = Student::new;

等价于:

Supplier<Student> supplier = () -> new Student();

有参构造:

Function<String, Student> creator = Student::new;

等价于:

Function<String, Student> creator =
        name -> new Student(name);

构造方法引用具体匹配哪个构造器,由函数式接口的参数列表决定。

4.5 thissuper 方法引用

当前对象的方法:

this::handle

父类对象的方法:

super::handle

this::方法名super::方法名 只能在相应的实例上下文中使用。

4.6 返回值可以被忽略

Consumer<T> 的抽象方法没有返回值,但它可以引用一个有返回值的方法,返回值会被直接忽略。

public class MethodReferenceDemo {

    public static void main(String[] args) {
        Consumer<Object> consumer1 =
                MethodReferenceDemo::print1;

        Function<Object, Integer> function =
                MethodReferenceDemo::print2;

        Consumer<Object> consumer2 =
                MethodReferenceDemo::print2;

        consumer1.accept("Java");
        function.apply("Spring Boot");
        consumer2.accept("Docker");
    }

    private static void print1(Object value) {
        System.out.println(value);
    }

    private static int print2(Object value) {
        System.out.println(value);
        return 1;
    }
}

4.7 方法引用的使用条件

方法引用需要满足:

  1. 目标类型必须是函数式接口。
  2. 参数数量和参数类型能够匹配。
  3. 返回值类型能够兼容。
  4. 构造方法引用需要存在与函数式接口参数匹配的构造器。

方法引用本身不要求类必须有无参构造方法。只有使用 Supplier<Student> supplier = Student::new 时,才要求存在无参构造器。


5. 自定义函数式接口

函数式接口有且只有一个抽象方法。

@FunctionalInterface
public interface Calculator {

    int calculate(int a, int b);
}

使用:

Calculator calculator = (a, b) -> a + b;

int result = calculator.calculate(10, 20);

@FunctionalInterface 不是必须的,但推荐添加。它能让编译器帮助检查接口是否满足函数式接口要求。


6. Java 8 常用函数式接口

这些接口都属于 java.util.function,是 Java 8 自带的,不需要引入第三方依赖

接口参数返回值常见用途
Runnable执行任务
Callable<V>V执行并返回结果,可抛异常
Comparator<T>两个 Tint排序和比较
Consumer<T>T消费数据
Supplier<T>T提供数据
Function<T, R>TR类型转换
Predicate<T>Tboolean条件判断
UnaryOperator<T>TT同类型转换
BinaryOperator<T>两个 TT同类型计算

6.1 Predicate

Predicate<Integer> positive =
        number -> number > 0;

boolean result = positive.test(10);

6.2 Consumer

Consumer<String> printer =
        value -> System.out.println(value);

printer.accept("Hello Java");

6.3 Supplier

Supplier<String> supplier =
        () -> "default value";

String value = supplier.get();

6.4 Function

Function<String, Integer> length =
        String::length;

Integer result = length.apply("Java");

7. 泛型与函数式接口

7.1 固定类型场景

参数类型明确时,直接使用具体泛型类型。

public boolean judge(
        Integer value,
        Predicate<Integer> predicate) {

    return predicate.test(value);
}

调用:

boolean result = judge(
        10,
        number -> number > 5
);

7.2 多类型复用场景

方法确实需要支持多种类型时,在方法上声明泛型:

public <T> boolean judge(
        T value,
        Predicate<T> predicate) {

    return predicate.test(value);
}

调用:

boolean numberResult =
        judge(10, number -> number > 5);

boolean stringResult =
        judge("Java", value -> value.length() > 3);

7.3 不推荐的写法

public <T> boolean judge(
        Integer value,
        Predicate<T> predicate) {

    // value 是 Integer,但 predicate 接收 T,
    // 两者没有建立类型约束,设计不合理。
    return false;
}

参数已经固定为 Integer,就不应该再使用无关的 <T>

推荐:

public boolean judge(
        Integer value,
        Predicate<Integer> predicate) {

    return predicate.test(value);
}

7.4 类型推断错误示例

judge(1, value -> value.length() > 0);

这里第一个参数是 Integer,所以 Lambda 参数也会被推断为 IntegerInteger 没有 length() 方法,因此编译失败。


8. 闭包

闭包可以理解为:

函数对象与它引用的外部变量绑定在一起形成的整体。

int x = 10;

Function<Integer, Integer> add =
        y -> x + y;

这里的 Lambda 捕获了外部变量 x

Java 要求被 Lambda 捕获的局部变量必须是:

  • final
  • 或者事实上的 final,即 effectively final

下面的写法不能编译:

int x = 10;

Function<Integer, Integer> add =
        y -> x + y;

x = 20;

因为 x 后续被修改,不再是 effectively final

注意:对象引用不能重新赋值,不代表对象内部状态不能修改。但在函数式编程中,仍然建议尽量避免修改外部状态。


9. 柯里化

柯里化是把接收多个参数的函数,拆分成多个每次只接收一个参数的函数。

Function<Integer, Function<Integer, Integer>> add =
        x -> y -> x + y;

调用:

int result = add.apply(2).apply(3);

执行过程:

Function<Integer, Integer> addTwo =
        add.apply(2);

int result = addTwo.apply(3);

可以理解为:

x -> y -> x + y

等价于:

x -> (y -> x + y)

其中:

Function<Integer, Function<Integer, Integer>>

表示:

  • 第一个 Integer 是输入参数 x
  • 返回值是另一个 Function<Integer, Integer>
  • 第二个函数接收 y,返回 x + y

10. 高阶函数

高阶函数通常满足至少一个条件:

  1. 接收函数对象作为参数;
  2. 返回一个函数对象。

接收函数对象:

public static int calculate(
        int a,
        int b,
        BinaryOperator<Integer> operator) {

    return operator.apply(a, b);
}

调用:

int sum = calculate(10, 20, (a, b) -> a + b);
int product = calculate(10, 20, (a, b) -> a * b);

返回函数对象:

public static Function<Integer, Integer> multiplyBy(
        int multiplier) {

    return value -> value * multiplier;
}

11. Stream API

Stream API 用于对集合数据进行声明式处理。

List<Integer> result = numbers.stream()
        .filter(number -> number > 0)
        .map(number -> number * 2)
        .collect(Collectors.toList());

11.1 Stream 的特点

  1. Stream 本身不存储数据。
  2. Stream 通常不会直接修改原集合。
  3. 中间操作具有延迟执行特性。
  4. 终止操作执行后,Stream 不能再次使用。

12. 常用 Stream 操作

12.1 filter

根据条件过滤元素,可以连续使用多次。

List<Integer> result = numbers.stream()
        .filter(number -> number > 0)
        .filter(number -> number % 2 == 0)
        .collect(Collectors.toList());

12.2 map

把每个元素转换成另一个元素。

List<String> names = students.stream()
        .map(Student::getName)
        .collect(Collectors.toList());

12.3 flatMap

flatMap 先把每个元素映射为一个流,再把多个流合并成一个流。

适用场景:

  • List<List<T>>
  • 二维数组;
  • 对象内部包含集合属性;
  • 字符串拆分后合并。
List<List<String>> nestedList = Arrays.asList(
        Arrays.asList("Java", "Spring"),
        Arrays.asList("Docker", "MySQL")
);

List<String> result = nestedList.stream()
        .flatMap(Collection::stream)
        .collect(Collectors.toList());

结果:

Java
Spring
Docker
MySQL

12.4 查找元素

Optional<Integer> first =
        numbers.stream().findFirst();

Optional<Integer> any =
        numbers.stream().findAny();

区别:

  • findFirst():返回遇到的第一个元素;
  • findAny():允许返回任意元素,并行流中通常更容易优化。

不要直接调用 Optional.get(),推荐:

Integer value = first.orElse(0);

12.5 匹配判断

boolean anyPositive = numbers.stream()
        .anyMatch(number -> number > 0);

boolean allPositive = numbers.stream()
        .allMatch(number -> number > 0);

boolean noneNegative = numbers.stream()
        .noneMatch(number -> number < 0);

含义:

  • anyMatch:至少有一个满足条件;
  • allMatch:全部满足条件;
  • noneMatch:全部不满足条件。

13. 创建 Stream

13.1 从集合创建

Stream<String> stream = list.stream();

13.2 从数组创建

Stream<String> stream = Arrays.stream(array);

13.3 使用 Stream.of

Stream<String> stream =
        Stream.of("Java", "Spring", "Docker");

13.4 基本类型流

IntStream stream = IntStream.range(0, 5);

结果范围:

0、1、2、3、4

range 包含起始值,不包含结束值。

IntStream stream = IntStream.rangeClosed(0, 5);

rangeClosed 同时包含起始值和结束值。


14. Stream 合并与截取

14.1 合并流

正确方法名是 concat,不是 cancat

Stream<String> stream1 =
        Stream.of("Java", "Spring");

Stream<String> stream2 =
        Stream.of("Docker", "MySQL");

Stream<String> result =
        Stream.concat(stream1, stream2);

14.2 跳过元素

Stream<Integer> result =
        numbers.stream().skip(2);

跳过前两个元素。

14.3 限制数量

Stream<Integer> result =
        numbers.stream().limit(5);

最多保留前五个元素。

14.4 takeWhiledropWhile

takeWhile(predicate)
dropWhile(predicate)

这两个方法是 Java 9 才加入的,Java 8 不能直接使用。

含义:

  • takeWhile:从开头连续获取满足条件的元素,一旦遇到不满足的元素就停止;
  • dropWhile:从开头连续丢弃满足条件的元素,一旦遇到不满足的元素,就保留后续元素。

Java 8 项目不要直接使用这两个方法,否则会编译失败。


15. 生成 Stream

15.1 iterate

根据上一个元素生成下一个元素。

Stream<Integer> stream = Stream.iterate(
        0,
        number -> number + 2
);

List<Integer> result = stream
        .limit(5)
        .collect(Collectors.toList());

结果:

0、2、4、6、8

iterate 默认会生成无限流,所以通常要搭配 limit

15.2 generate

通过 Supplier 持续生成元素。

Stream<Double> stream =
        Stream.generate(Math::random);

List<Double> result = stream
        .limit(5)
        .collect(Collectors.toList());

generate 同样会创建无限流,需要使用 limit 限制数量。

15.3 ThreadLocalRandom

int[] numbers = ThreadLocalRandom.current()
        .ints(5, 0, 100)
        .toArray();

含义:

  • 生成 5 个随机整数;
  • 最小值包含 0
  • 最大值不包含 100

ThreadLocalRandom 是 Java 7 引入的,属于 JDK 自带类

java.util.concurrent.ThreadLocalRandom

它适合多线程环境,每个线程使用相对独立的随机数状态,可以减少多个线程争用同一个随机数生成器的问题。

补充修正:java.util.Random 的核心种子更新主要使用原子 CAS 操作,并不是简单地通过 synchronized 锁完成。但多个线程共享同一个 Random 时,仍可能竞争同一个原子种子,导致性能下降。


16. 排序与比较

16.1 Comparator

List<Student> students = new ArrayList<Student>();

students.sort(
        Comparator.comparing(Student::getAge)
);

倒序:

students.sort(
        Comparator.comparing(Student::getAge)
                .reversed()
);

多字段排序:

students.sort(
        Comparator.comparing(Student::getAge)
                .thenComparing(Student::getName)
);

Comparator.compare(a, b) 通常遵循:

  • 返回负数:a 排在 b 前面;
  • 返回 0:两者在排序意义上相等;
  • 返回正数:a 排在 b 后面。

17. Java 8 使用注意事项

17.1 Stream 不能重复使用

错误示例:

Stream<Integer> stream = numbers.stream();

stream.filter(number -> number > 0)
        .count();

stream.filter(number -> number < 0)
        .count();

第二次操作会抛出异常,因为 Stream 已经被消费。

正确做法:

long positiveCount = numbers.stream()
        .filter(number -> number > 0)
        .count();

long negativeCount = numbers.stream()
        .filter(number -> number < 0)
        .count();

17.2 不要在 Stream 中修改外部集合

不推荐:

List<Integer> result = new ArrayList<Integer>();

numbers.stream()
        .filter(number -> number > 0)
        .forEach(result::add);

推荐:

List<Integer> result = numbers.stream()
        .filter(number -> number > 0)
        .collect(Collectors.toList());

17.3 不要为了使用 Stream 而使用 Stream

简单循环往往更容易理解:

for (Student student : students) {
    student.setEnabled(true);
}

没有必要强行改成复杂的 Stream 链式调用。

17.4 注意装箱和拆箱

Stream<Integer>

会涉及 intInteger 的装箱和拆箱。

大量数字计算优先考虑:

IntStream
LongStream
DoubleStream

18. 总结

Lambda 表达式

(参数) -> 表达式

Lambda 用于创建函数式接口对象。

方法引用

类名::静态方法
对象名::实例方法
类名::实例方法
类名::new

方法引用是满足签名匹配条件时,对 Lambda 的进一步简化。

函数式接口

函数式接口只有一个抽象方法,Java 8 常用接口包括:

Predicate
Consumer
Supplier
Function
UnaryOperator
BinaryOperator
Comparator
Runnable
Callable

Stream API

常见处理流程:

数据源
  ↓
创建 Stream
  ↓
filter / map / flatMap / sorted
  ↓
collect / count / findFirst / reduce

核心原则

  1. 固定类型场景不要滥用泛型。
  2. Lambda 捕获的局部变量必须是 finaleffectively final
  3. Stream 中间操作延迟执行,终止操作触发计算。
  4. Stream 使用一次后不能重复消费。
  5. Java 8 不支持 takeWhiledropWhile

已发布

分类

来自

标签: