FP since Java8

作者:lanran 发布于:2025年8月15日

Java函数式,带着脚镣跳舞

目录 12 节
  1. Java8
  2. 从匿名类到 lambda
  3. Function<T, R>
  4. BiFunction 和 Operator
  5. Supplier
  6. Consumer
  7. Predicate
  8. Comparator
  9. 方法引用
  10. 基本类型特化接口
  11. Runnable
  12. 小结

Java8

Java 8 引入 lambda 和 Stream 以后,Java 终于能写出一些函数式风格的代码了。但 Java 的函数式并不是“函数就是值”那种路线。它没有给函数一个原生的函数类型,而是选择了一个更 Java 的方案:函数式接口

这里说 since Java8,主要是从 Java 8 引入 lambda 和函数式接口开始讲。后面的例子会顺手使用一些新版本语法,比如 List.ofrecordStream.toList(),核心讨论的仍然是 Java 8 以来的函数式接口体系。

函数式接口(Functional Interface)指的是只有一个抽象方法的接口。lambda 表达式本身不能单独存在,它必须被放进某个目标类型里。这个目标类型通常就是 FunctionConsumerPredicateSupplier 这一类接口。

比如:

Function<String, Integer> length = s -> s.length();

Integer n = length.apply("hello");
System.out.println(n); // 5

这里的 s -> s.length() 看起来像一个函数,但在 Java 里,它最终还是要落到 Function<String, Integer> 这个接口上。可以说 Java 8 引入了函数式能力,但它没有彻底离开面向对象的类型系统。

这也是我说 Java 函数式有点“带着脚镣跳舞”的原因:它能跳,而且很多时候跳得还不错,但每一步都要先问一句:这个 lambda 要被哪个函数式接口接住?

从匿名类到 lambda

在 Java 8 之前,我们经常用匿名类表达“传一段行为”。

Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("running");
    }
};

task.run();

Java 8 以后可以写成:

Runnable task = () -> System.out.println("running");

task.run();

本质上还是实现 Runnable 这个接口,只是 lambda 把匿名类那层模板代码抹掉了。

这也是理解 Java 8 FP 的入口:lambda 不是脱离类型系统的自由函数,它是函数式接口的一个实现。

递归 lambda 也能看出这种限制。下面这种直接自引用的写法在 Java 里并不自然:

Function<Integer, Integer> factorial =
    n -> n <= 1 ? 1 : n * factorial.apply(n - 1);

因为变量 factorial 还在初始化过程中,lambda 里不能直接这样引用它。比较朴素的写法还是匿名类:

Function<Integer, Integer> factorial = new Function<Integer, Integer>() {
    @Override
    public Integer apply(Integer n) {
        if (n <= 1) {
            return 1;
        }
        return n * apply(n - 1);
    }
};

System.out.println(factorial.apply(5)); // 120

这一点也挺 Java:普通场景里 lambda 很舒服,一旦碰到更函数式的表达,就会看到接口和变量初始化规则的边界。

Function<T, R>

Function<T, R> 是最典型的函数式接口。它表达的是一个从 TR 的映射。

最小形状大概是:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

比如把字符串转成长度:

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

System.out.println(length.apply("hello")); // 5

或者把字符串解析成数字:

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

System.out.println(parse.apply("42")); // 42

Function 真正有意思的地方在于组合。andThen 是先执行当前函数,再执行后一个函数。

Function<String, Integer> parse = Integer::parseInt;
Function<Integer, Integer> square = x -> x * x;

Function<String, Integer> parseThenSquare = parse.andThen(square);

System.out.println(parseThenSquare.apply("12")); // 144

compose 则反过来,先执行传进来的函数。

Function<String, String> trim = String::trim;
Function<String, Integer> parse = Integer::parseInt;

Function<String, Integer> trimThenParse = parse.compose(trim);

System.out.println(trimThenParse.apply(" 42 ")); // 42

Function<T, T> 这种输入输出同类型的情况,Java 又给了一个更具体的名字:UnaryOperator<T>

UnaryOperator<String> normalize =
    s -> s.trim().toLowerCase();

System.out.println(normalize.apply("  HELLO  ")); // hello

它没有增加本质能力,只是让类型读起来更明确:这是一个同类型转换。

BiFunction 和 Operator

一个参数不够用时,可以用 BiFunction<T, U, R>

BiFunction<Integer, Integer, Integer> add =
    (a, b) -> a + b;

System.out.println(add.apply(10, 20)); // 30

如果两个参数和返回值都是同一种类型,就可以用 BinaryOperator<T>

BinaryOperator<Integer> max = Integer::max;

System.out.println(max.apply(3, 9)); // 9

BinaryOperatorreduce 里很常见。

List<Integer> nums = List.of(1, 2, 3, 4);

Integer sum = nums.stream()
    .reduce(0, Integer::sum);

System.out.println(sum); // 10

Java 标准库没有提供三参数版本的 TriFunction。如果真的需要,可以自己写一个:

@FunctionalInterface
public interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

然后这样使用:

TriFunction<Integer, Integer, Integer, Integer> volume =
    (width, height, depth) -> width * height * depth;

System.out.println(volume.apply(10, 20, 30)); // 6000

当然,也可以用柯里化把多参数拆开:

Function<Integer, Function<Integer, Function<Integer, Integer>>> volume =
    width -> height -> depth -> width * height * depth;

System.out.println(volume.apply(10).apply(20).apply(30)); // 6000

这能写,但不一定好读。Java 的类型噪音会很快冒出来。

Supplier

Supplier<T> 表达的是“不给参数,产出一个值”。

最小形状是:

@FunctionalInterface
public interface Supplier<T> {
    T get();
}

比如生成一个 UUID:

Supplier<UUID> idGenerator = UUID::randomUUID;

System.out.println(idGenerator.get());

它经常用在“延迟创建”的地方。比如日志、缓存、默认值,很多时候不希望值一开始就被计算出来,而是等真正需要时再调用 get()

String value = Optional.<String>empty()
    .orElseGet(() -> expensiveDefaultValue());

这里如果用 orElse(expensiveDefaultValue()),默认值会先被算出来;用 orElseGet,则只有在 Optional 为空时才会调用 supplier。

Consumer

Consumer<T> 表达的是“接收一个值,但不返回结果”。

最小形状是:

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

最常见的例子就是遍历输出:

List<String> names = List.of("alice", "bob", "carol");

names.forEach(System.out::println);

也可以把多个消费动作串起来:

Consumer<String> print = System.out::println;
Consumer<String> log = s -> System.out.println("log: " + s);

Consumer<String> printAndLog = print.andThen(log);

printAndLog.accept("hello");

Consumer 适合表达副作用:打印、写日志、发送消息、更新外部状态。它不是纯函数式里最受欢迎的那类东西,但在 Java 这种工程语言里很实用。

两个参数版本是 BiConsumer<T, U>

BiConsumer<String, Integer> printEntry =
    (name, age) -> System.out.println(name + ": " + age);

printEntry.accept("alice", 18);

Predicate

Predicate<T> 表达的是“判断一个值是否满足条件”。

最小形状是:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

它在 filter 里最常见:

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0)
    .toList();

System.out.println(evens); // [2, 4, 6]

Predicate 也可以组合。

Predicate<String> notBlank = s -> !s.trim().isEmpty();
Predicate<String> shorterThan10 = s -> s.length() < 10;

Predicate<String> validName = notBlank.and(shorterThan10);

System.out.println(validName.test("lanran")); // true

还有 ornegate

Predicate<String> startsWithA = s -> s.startsWith("a");
Predicate<String> startsWithB = s -> s.startsWith("b");

Predicate<String> startsWithAOrB = startsWithA.or(startsWithB);

System.out.println(startsWithAOrB.test("bob")); // true
System.out.println(startsWithA.negate().test("bob")); // true

两个参数版本是 BiPredicate<T, U>

BiPredicate<String, Integer> longerThan =
    (s, len) -> s.length() > len;

System.out.println(longerThan.test("hello", 3)); // true

Comparator

Comparator<T> 也可以看成一个函数式接口,只是它的语义更具体:比较两个值的顺序。

Comparator<String> byLength =
    (a, b) -> Integer.compare(a.length(), b.length());

Java 8 以后,排序代码变得舒服很多。

List<String> names = new ArrayList<>(List.of("bob", "alice", "carol"));

names.sort(Comparator.comparing(String::length));

System.out.println(names); // [bob, alice, carol]

也可以链式排序:

record User(String name, int age) {}

List<User> users = new ArrayList<>(List.of(
    new User("alice", 18),
    new User("bob", 18),
    new User("carol", 20)
));

users.sort(
    Comparator.comparing(User::age)
        .thenComparing(User::name)
);

这里 User::ageUser::name 都是方法引用。它们不是直接执行方法,而是把“如何取排序 key”这段逻辑传给 Comparator.comparing

方法引用

lambda 很多时候还能再简化成方法引用。

Function<String, Integer> length1 = s -> s.length();
Function<String, Integer> length2 = String::length;

String::length 看起来很短,但它背后还是要落到某个函数式接口上。在这里,目标类型是:

Function<String, Integer>

再比如:

Consumer<String> printer = System.out::println;

printer.accept("hello");

方法引用有几种常见形式:

String::length        // 未绑定实例方法
System.out::println   // 绑定实例方法
Integer::parseInt     // 静态方法
ArrayList::new        // 构造器引用

这也能看出 Java 的特点:语法上看起来像函数,但类型上还是必须被某个函数式接口接住。

基本类型特化接口

Java 的泛型不能直接处理基本类型,所以如果只用 Function<Integer, Integer> 这类接口,会有装箱和拆箱成本。

为了减少这种成本,Java 提供了一批基本类型特化接口,比如:

IntFunction<R>       // int -> R
ToIntFunction<T>     // T -> int
IntUnaryOperator     // int -> int
IntPredicate         // int -> boolean
IntConsumer          // int -> void
IntSupplier          // void -> int

实际代码里,如果你在处理大量 intlongdouble,这些接口和 IntStreamLongStreamDoubleStream 会更合适。

int sum = IntStream.of(1, 2, 3, 4)
    .map(x -> x * x)
    .sum();

System.out.println(sum); // 30

这部分不用死记。看到 IntLongDouble 开头的函数式接口,大体就知道它是在绕开装箱成本。

Runnable

Runnable 是 Java 里非常早就存在的接口,但从函数式接口的角度看,它也是一个 void -> void

@FunctionalInterface
public interface Runnable {
    void run();
}

所以它可以直接用 lambda 表达:

Runnable task = () -> System.out.println("running");

new Thread(task).start();

它没有输入,也没有返回值,只表达一个动作。

小结

Java 8 的函数式接口大概可以这样记:

Function<T, R>       T -> R
Supplier<T>          void -> T
Consumer<T>          T -> void
Predicate<T>         T -> boolean
BiFunction<T, U, R>  (T, U) -> R
Comparator<T>        (T, T) -> int
Runnable             void -> void

Java 的 FP 能力很实用。mapfilterreducesort、方法引用,这些东西让日常代码清爽了不少。

但它也不是那种无拘无束的函数式。lambda 需要目标类型,方法引用需要函数式接口,基本类型还要一堆特化接口。它最终还是站在 Java 的类型系统里。

所以 Java 8 的函数式更像是一种折中:不纯粹,但够工程;不自由,但很实用。带着脚镣跳舞,有时候也确实能跳得不错。