番石榴为什么toStringFunction不是通用函数?

时间:2015-03-18 23:40:06

标签: java generics guava

Guava toStringFunction()具有以下声明:

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

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

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

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

其中someMap变量是map&lt; String,Double&gt;,所以我希望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.如何具体告诉编译器toStringFunction应该是Function&lt;整数,字符串&gt;?简单的演员阵容不起作用,我正在寻找两个函数的真实组合。

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

// cause error 

2.将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());

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

4 个答案:

答案 0 :(得分:7)

toStringFunction()Function<Object, String>而不是Function<T, String>的原因非常简单,因为您可以在任何toString()上致电Object。它实际上是一个接受对象并返回字符串的函数。番石榴避免将类型参数(和通配符泛型)引入不起作用的方法。

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

此外,假设正确使用泛型,您的函数类型不应该是Function<Integer, Double>而不是Function<Object, Double>,因为大多数代码接受函数并使用它来执行某事应该接受Function<? super F, ? extends T>之类的东西。所以真正的问题是“为什么要将Function<Object, Double>称为Function<Integer, Double>”?

答案 1 :(得分:4)

在评论中讨论后:

修复编译错误:

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

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

对于特定的组合,通过将Function的输入参数从Object更改为Integer,你什么也得不到,因为Function只需要Object中的方法,所以这是一个很好的决定,因为它简化了这种情况下的事情并且使得它更广泛使用。假设您能够将参数类型强制为Integer,您仍然无法获得任何好处,无论是IllegalArgumentException的验证还是您正在尝试的其他内容。

在设计API时,您希望尽可能多的消费者使用它,而不必偏离您的方式,即您应该使用满足您要求的最高级别的类。通过在toStringFunction中使用Object,Guava函数满足了这个原则。这使得该功能更通用并且可以更广泛地使用。这就是选择Object而不是参数化此函数的原因。

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

原始答案:

每个类都是Object的子类,Function只调用Object类中存在的输入上的toString。因此,在这种情况下,声明一个Function等同于声明Function。通过将Function的输入参数从Object更改为Integer,您将无法获得任何结果,因为Function只需要Object中的方法,所以这是一个很好的决定,因为它简化了这种情况下的事情。

您应尽可能使用满足您要求的最高级别课程。这使得该功能更通用并且可以更广泛地使用。这就是选择Object而不是参数化此函数的原因。

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

答案 2 :(得分:4)

Guava没有将其声明为Function<T, String>(尽管可以这样做)的原因是由comment to a similar question in their bug tracker中的一位番石榴团队成员解释的:

  

我们认为库API中类型参数和通配符的目的是确保可以使用任何具有逻辑意义的参数类型调用方法。一旦满足这个条件,我们就会停止,而不是继续添加类型参数/通配符,只是为了让你按摩&#34;您想要获得的特定结果类型。

所以基本上他们认为这不值得。不应更改toStringFunction之类的方法以返回Function<T, String>,而是应更改泛型类型的使用者,以便允许他们获得Function<? super F, ? extends T>

答案 3 :(得分:1)

关于番石榴队这种行为的动机,我真的不知道。也许其中一个人应该回应。

关于编译错误,您需要一个帮助方法才能执行强制转换:

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 

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

Specifficaly,我正在从Function<Object, Double>投射到Function<Integer, Double>。我将其视为缩小类型转换,因此我认为这是合法且安全的。如果我对此感到满意,请告诉我,是否值得承担风险。