Guava 为什么 toStringFunction 不是泛型函数?

Guava why toStringFunction not a generic function?

Guava toStringFunction() 具有以下声明:

public static Function<Object, String> toStringFunction() { ... } 

Object的所有非原始根,所以函数运行良好。

但是当我尝试将它与另一个函数组合时,例如:

Function<Integer, Double> f1 = Functions.compose(Functions.forMap(someMap),
                                                 Functions.toStringFunction());

其中 someMap 变量是一个 map,所以我希望 toStringFunction 将 Integer 转换为 String,然后 forMap 将 String 转换为 Double。 但是我得到一个编译器错误:

    Function<Integer, Double> f1 = Functions.compose(Functions.forMap(someMap),
                                                    ^
  required: Function<Integer,Double>
  found:    Function<Object,Double>
2 errors

我的两个问题是:

1.how专门告诉编译器toStringFunction应该是Function?简单的转换是行不通的,我正在寻找这两个函数的真正组合。

Function< Integer, String> g1 = (Function< Integer, String>)Functions.toStringFunction(); 

// cause error 

2.Had toStringFunction 写成如下:

 @SuppressWarnings("unchecked") 
  public static <E> Function<E, String> toStringFunction(){
    return (Function<E, String>) ToStringFunction.INSTANCE;
  }

  // enum singleton pattern
  private enum ToStringFunction implements Function<Object, String>{
    INSTANCE;


    // @Override public String toString(){
    //   return "Functions.toStringFunction()";
    // }

    @Override public String apply(Object o){
      return o.toString();
    }
  }

我可以指定类型:

Function<Integer, Double> f1 = Functions.compose(Functions.forMap(someMap),
                                                 Functions.<Integer>toStringFunction());

这很好用。但是 Guava 团队现在拥有的版本的动机是什么?

经过评论讨论:

修复编译错误:

Function<Object, Double> f1 = 
    Functions.compose(Functions.forMap(someMap), Functions.toStringFunction());

你的问题暗示你想要 Function<Integer, Double> 而不是 Function<Object, Double>。根据评论中的讨论,并尝试考虑其他场景,您想要这样做的唯一原因是为了验证。 toStringFunction 中的代码不受您的控制,因此该函数中的验证是不可能的。至于 forMap 函数中的验证,该代码也不在您的控制范围内。如果您想防止 IllegalArgumentException,则无论您提供的输入是对象还是整数,都必须执行此验证。因此验证也超出了 forMap、toStringFunction 和 compose 的范围,因为 none 这些函数提供了这种验证,您必须编写自己的代码来执行此操作(要么捕获异常并处理它或在调用此组合函数之前以某种方式预先验证)。

对于具体的compose,把Function的入参从Object改成Integer对你没有任何好处,因为Function只需要Object中存在的方法,所以这是一个很好的决定,因为它在这种情况下简化了事情并使其得到更广泛的使用。假设您能够将参数类型强制为 Integer,您仍然没有获得任何好处,无论是对 IllegalArgumentException 的验证还是您正在尝试的其他东西。

在设计 API 时,您希望它可以被尽可能多的消费者使用,而不必费力,即您应该使用满足以下要求的最高级别 class你的要求。通过在 toStringFunction 中使用 Object,Guava 函数满足了这个原则。这使得函数更通用,使用更广泛。这就是选择对象而不是参数化此函数的原因。

如果 Function 接受一个参数,它所做的与现在没有什么不同,所以为什么在不需要参数时使用参数。这种方法大大简化了设计,而不是添加不需要的东西。

原回答:

每个 class 都是 Object 的子对象,函数仅在对象 class 中存在的输入上调用 toString。所以在这种情况下,声明一个Function就等同于声明Function。将 Function 的输入参数从 Object 更改为 Integer 不会有任何好处,因为 Function 只需要 Object 中存在的方法,所以这是一个很好的决定,因为它在这种情况下简化了事情。

尽可能使用满足您要求的最高级别class。这使得函数更通用,使用更广泛。这就是选择对象而不是参数化此函数的原因。

修复编译错误:Function f1 = Functions.compose(Functions.forMap(someMap), Functions.toStringFunction());

关于Guava团队这种行为的动机,我真的不知道。也许他们中的一个应该回应。

关于编译错误,您需要一个辅助方法来执行转换:

public class Sample {

    @SuppressWarnings("unchecked")
    private static <T> Function<T, String> checkedToStringFunction() {
        return (Function<T, String>) Functions.toStringFunction();
    }

    public static void main(String[] args) {
        Map<String, Double> someMap = new HashMap<>();

        Function<Integer, Double> f1 = Functions.compose(
            Functions.forMap(someMap), 
            checkedToStringFunction()); // compiles fine
    }
}

这样,编译器就可以安全地推断出参数类型。

编辑:

有人告诉我这种技术可能会导致 堆污染。虽然从理论的角度来看这可能是正确的,但在实践中,我看不出这会如何导致 ClassCastException 或任何其他运行时错误,因为辅助方法是 private 并且仅用于推断 Function 类型参数。

如果我强制使用通用类型,即:

Function<Integer, Double> f1 = Functions.compose(
    Functions.forMap(someMap), 
    Sample.<BigInteger> checkedToStringFunction()); // compilation error

我会遇到编译错误。要修复它,我需要将 f1 Function 的第一个类型参数更改为:

Function<BigInteger, Double> f1 = Functions.compose(
    Functions.forMap(someMap), 
    Sample.<BigInteger> checkedToStringFunction()); // compiles fine

注:

我知道什么是堆污染:

private static <S, T> S convert(T arg) { 
    return (S) arg;  // unchecked warning 
} 

Number n = convert(new Long(5L)); // fine 
String s = convert(new Long(5L)); // ClassCastException 

在此示例中,我正在转换为给定类型变量,而在我建议的解决方案中,我正在转换为参数化类型。

具体来说,我正在从 Function<Object, Double> 转换为 Function<Integer, Double>。我将其视为 narrowing 类型转换,因此我相信它是合法且安全的。请让我知道我是否可以接受,是否值得冒这个险。

toStringFunction()Function<Object, String> 而不是 Function<T, String> 的原因很简单,因为您可以在任何 Object 上调用 toString()。它实际上是一个接受对象和 returns 字符串的函数。 Guava 避免将类型参数(和通配符泛型)引入无用的方法。

您的示例中的问题是您试图将 f1 函数键入 Function<Integer, Double> 而实际上它是 Function<Object, Double>:它可以接受任何对象,调用 toString() 上,将该字符串传递给您的 forMap 函数并获得结果。

此外,假设正确使用泛型,您的函数类型不应该是 Function<Integer, Double> 而不是 Function<Object, Double>,因为大多数接受函数并使用它的代码做某事应该接受类似 Function<? super F, ? extends T> 的东西。所以这里真正的问题是 "why do you want to refer to a Function<Object, Double> as a Function<Integer, Double>"?

Guava 不将其声明为 Function<T, String> 的原因(尽管它可以完成)由一位 Guava 团队成员在 comment to a similar question in their bug tracker:

中解释

We decided that the purpose of type parameters and wildcards in a library API is to ensure that the method can be called with any parameter types that make logical sense. Once that condition is met, we stop, instead of continuing to also add type params/wildcards that serve only to let you "massage" the specific result type you want to get.

所以基本上他们认为这是不值得的。与其将 toStringFunction 之类的方法更改为 return 一个 Function<T, String>,不如更改泛型类型的消费者,以便它们允许被赋予 Function<? super F, ? extends T>.