FP since Java8

Java函数式,带着脚镣跳舞

917 words 9 min read
目录 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 的函数式并非“函数是一等公民”的实现。它没有给出一个原生的函数类型,而是选择了一个向前兼容的方案:函数式接口

函数式接口(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 虽然引入了函数式能力,但它没有离开面向对象的类型系统。

从匿名类到 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的限制,在fp里如果想要实现定义递归的函数类型,需要用到一个叫 Y Combinator的组合子,此处略。

Function<T, R>

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

@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>: T -> 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>:(T, T) -> 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

当然,也可以手动柯里化把多参数拆开(柯里化这种fp风格很强的技巧不建议在java里使用,语法会很冗长):

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

Supplier

Supplier<T> 是“生产者接口”,没有入参,会返回一个值:() -> 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> 是“消费者接口”,消费一个值: 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 适合表达副作用:打印、写日志、发送消息、更新外部状态。

两个参数版本是 BiConsumer<T, U>: (T, U) -> ()

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

printEntry.accept("alice", 18);

Predicate

Predicate<T> 一元谓词,断言是否为真: T -> boolean

@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>: (T, U) -> boolean

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 的泛型不能直接处理基本类型,所以如果只用 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

Runnable

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

@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>          () -> T
Consumer<T>          T -> ()
Predicate<T>         T -> boolean
BiFunction<T, U, R>  (T, U) -> R
Comparator<T>        (T, T) -> int
Runnable             () -> ()

mapfilterreducesort、方法引用,减少了大量样板代码,让写代码更加声明式,可组合。

但它也不是完全的函数式。lambda 需要目标类型,方法引用需要函数式接口,基本类型还要一堆特化接口。它最终还是被限制在 Java 的类型系统里。

Java 8 的函数式是一种折中设计:不纯粹,但工程;不自由,但实用。