FP since Java8
Java函数式,带着脚镣跳舞
目录
Java8
自从 Java 8 引入 lambda 和 Stream 以后,Java 终于能写出一些函数式风格的代码了,你再也不用忍受冗余的匿名类样板代码。但 Java 的函数式并非“函数是一等公民”的实现。它没有给出一个原生的函数类型,而是选择了一个向前兼容的方案:函数式接口。
函数式接口(Functional Interface)指的是只有一个抽象方法的接口。同时可以通过 lambda 表达式简化匿名函数的编写,但是本身不能单独存在,它必须被放进某个目标类型里。这个目标类型通常就是 Function、Consumer、Predicate、Supplier 这一类接口。
比如:
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> 是最典型的函数式接口。它表达的是一个从 T 到 R 的映射: 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
BinaryOperator 在 reduce 里很常见。
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
还有 or 和 negate。
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::age 和 User::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
实际代码里,如果你在处理大量 int、long、double,这些接口和 IntStream、LongStream、DoubleStream 会更适配。
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 () -> ()
map、filter、reduce、sort、方法引用,减少了大量样板代码,让写代码更加声明式,可组合。
但它也不是完全的函数式。lambda 需要目标类型,方法引用需要函数式接口,基本类型还要一堆特化接口。它最终还是被限制在 Java 的类型系统里。
Java 8 的函数式是一种折中设计:不纯粹,但工程;不自由,但实用。