FP since Java8
Java函数式,带着脚镣跳舞
目录
Java8
Java 8 引入 lambda 和 Stream 以后,Java 终于能写出一些函数式风格的代码了。但 Java 的函数式并不是“函数就是值”那种路线。它没有给函数一个原生的函数类型,而是选择了一个更 Java 的方案:函数式接口。
这里说 since Java8,主要是从 Java 8 引入 lambda 和函数式接口开始讲。后面的例子会顺手使用一些新版本语法,比如 List.of、record、Stream.toList(),核心讨论的仍然是 Java 8 以来的函数式接口体系。
函数式接口(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 引入了函数式能力,但它没有彻底离开面向对象的类型系统。
这也是我说 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> 是最典型的函数式接口。它表达的是一个从 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>。
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
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
当然,也可以用柯里化把多参数拆开:
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
还有 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>。
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 的特点:语法上看起来像函数,但类型上还是必须被某个函数式接口接住。
基本类型特化接口
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
这部分不用死记。看到 Int、Long、Double 开头的函数式接口,大体就知道它是在绕开装箱成本。
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 能力很实用。map、filter、reduce、sort、方法引用,这些东西让日常代码清爽了不少。
但它也不是那种无拘无束的函数式。lambda 需要目标类型,方法引用需要函数式接口,基本类型还要一堆特化接口。它最终还是站在 Java 的类型系统里。
所以 Java 8 的函数式更像是一种折中:不纯粹,但够工程;不自由,但很实用。带着脚镣跳舞,有时候也确实能跳得不错。