为什么Java 8中的Predicate<T>没有继承Function<T, Boolean>?

42
如果我编写Predicate接口,我希望在接口中编码它只是一个返回基本类型boolean的函数,就像这样:
@FunctionalInterface
public interface Predicate<T> extends Function<T, Boolean> {

    boolean test(T t);

    @Override
    default Boolean apply(T t) {
        return Boolean.valueOf(test(t));
    }
}

我在想,Java 8 API的设计者选择将Predicate与Function完全分开是否有令人信服的原因?他们是否考虑过这样做并决定不这样做?我猜类似的问题也适用于所有其他“特殊”的函数接口,如Consumer(可以是Function),Supplier(Function)和原始函数,如IntFunction(Function)。我没有深入和彻底地考虑所有这些问题,所以我可能会漏掉一些东西。编辑:一些答案强调了应用和测试之间的语义区别。我不是说我不欣赏这种区别,我同意这种区别是有益的。我的困惑是为什么Predicate仍然不像List是Collection或Double是Number那样也是Function in的一种方式,即Object。
如果Predicate(以及其他所有特殊的泛型函数接口,例如ConsumerSupplierIntUnaryOperator等)与Function有这种关系,那么它允许在期望Function参数的地方使用它(想到的是与其他函数的组合,例如调用myFunction.compose(myPredicate)或在API中避免编写多个专门的函数,当上述自动(解)装箱实现足够时)。

编辑2:查看openjdk lambda项目,我发现原始函数接口一直扩展Function,直到Brian Goetz在2012-12-19提交此更改。我找不到该时间段任何lambda-dev或JSR专家组邮件列表上的具体原因。


1
@brian-goetz,能否给出EDIT 2的规范答案? - Thorbjørn Ravn Andersen
4个回答

22
Predicate<T>中的方法返回boolean。而Function<T, Boolean>中的方法返回Boolean。它们不同。虽然有自动装箱,但Java方法在原始类型可用时不使用包装类。此外,还有一些区别,例如Boolean可以为null,而boolean不能。

Consumer<T>的情况下更加不同。 Consumer<T>中的方法返回类型为void,这意味着它可以隐式地返回或使用return;返回,但是Function<T, Void>中的方法必须明确地使用return null;返回。


13
这听起来不太令人信服。考虑到Java 8有默认方法,他们为什么不轻而易举地使 Predicate<T> 扩展 Function<T> 并确保 Predicate 中有一个默认的 apply 方法,该方法委托给 test? 这样消费者就可以简单地实现 test 同时也获得一致的 apply 语义的好处。 - lenkite

13

没有必要使用这样可疑的继承体系,这些函数式接口是可以互换的。

Function<A,Boolean> f1=…;
Predicate<A>        p1=…;

Predicate<A>        p2=f1::apply;
Function<A,Boolean> f2=p1::test;

这在双向上都可行。那么为什么要有一个继承关系来宣传一个特定方向呢?


6
在我看来,Function<T, R>只是一个通用函数的定义。如果所有的FunctionalInterfaces都实现了Function,那么唯一的抽象方法必须被命名为apply()。在具体的FunctionalInterface上下文中,如FilterFile,抽象方法boolean accept(File pathname)Boolean apply(File)更好地描述了其含义。
注释@FunctionalInterface已经标记了一个接口可以用作FunctionalInterface。没有必要让它们都实现一个基础接口,除非你想以通用的方式处理它们。我不认为你会不关心FunctionalInterface的语义,而想要提前调用它们的apply方法。

2
我并不是在主张所有的函数式接口都应该被替换成Function。我建议预定义的通用函数式接口可以扩展Function,这样可以保留语义同时允许额外的东西,比如在通用中进行处理(如果没有重大缺点,这似乎是一个相关的好处)。 - mkadunc

6

这并不是对你问题的直接回答,但是你想要用它来做什么呢?

考虑以下场景:您想将 true/false 映射到相应为 true 或 false 的值列表。

使用您的代码,可以使用以下方法:

@FunctionalInterface
interface CustomPredicate<T> extends Function<T, Boolean> {
    boolean test(T value);

    @Override
    default Boolean apply(T t) {
        return test(t);
    }
}

List<String> stringList = new ArrayList<>();
stringList.add("a");
stringList.add("hg");
stringList.add("dsl");
stringList.add("sldi");
stringList.add("ilsdo");
stringList.add("jlieio");
CustomPredicate<String> customPredicate = str -> (str.length() >= 3);
Map<Boolean, List<String>> mapping = stringList.stream()
        .collect(Collectors.groupingBy(customPredicate));

以下内容表明他们肯定考虑过类似的事情,因为他们提供了分区方法:
List<String> stringList = new ArrayList<>();
stringList.add("a");
stringList.add("hg");
stringList.add("dsl");
stringList.add("sldi");
stringList.add("ilsdo");
stringList.add("jlieio");
Predicate<String> predicate = str -> (str.length() >= 3);
Map<Boolean, List<String>> mapping = stringList.stream()
        .collect(Collectors.partitioningBy(predicate));

我能想到的一些原因是:

  • 在只期望有 test() 方法的 Predicate 中提供一个 apply() 方法并不直观。
  • 函数式接口旨在提供仅包含一种基本功能的接口(不包括链接或逻辑操作),在您的情况下,CustomPredicate 包含两种类型的功能。这只会增加混淆。

另一个很好的原因是我们可以避免不必要的装箱/拆箱。 - Edwin Dalorzo
@EdwinDalorzo 我并不建议用 apply() 方法替换 test() 方法,自动装箱和拆箱只会在对象被用于没有其他选择的谓词实现的情况下进行。接受泛型 Function<A, ?> 的函数可以始终为性能提供专门的 Predicate<A> 重载。 - mkadunc
1
@skiwi 我的建议只是用另一种非原始、更通用的方式表达相同的操作。类/接口经常有方法,仅仅是为了在更高的抽象层次上实现一个概念(PrimitiveIterator.OfInt扩展了Iterator并使用装箱;Deque扩展了Queue并用更合适的名称offerLast替换了offer())。 - mkadunc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接