Guava为什么toStringFunction不是一个通用函数?

7

Guava的toStringFunction()具有以下声明:

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

所有非原始根在Object中,因此该函数运行良好。
但是当我尝试将其与另一个函数组合使用时,例如:
Function<Integer, Double> f1 = Functions.compose(Functions.forMap(someMap),
                                                 Functions.toStringFunction());

在这里,someMap变量是一个map<String, Double>,因此我期望toStringFunction将整数转换为字符串,然后forMap将字符串转换为双精度浮点数。 但是,我收到了编译器错误:

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

我的两个问题是:
1.如何明确告诉编译器toStringFunction应该是Function < Integer,String>类型?简单的强制转换不起作用,我正在寻找这两个函数的真正组合。
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());

这个版本可以正常工作。但是Guava团队现在的版本有什么动机呢?


4
这并不回答你的问题“为什么”(所以我发表为评论),但是:Functions.compose在第二个参数的输出类型中使用了通配符以允许一定的灵活性,因此虽然你不能编写Functions.<Integer> toStringFunction(),但可以通过编写Functions.compose(Functions.toStringFunction(), Functions.<Integer> identity())来创建一个整数到字符串的函数。当然,在Java 8中,你可以直接写Integer::toString。 :-) - ruakh
如果一个 Map<K, V>Map.get(key) 方法中的 keyObject 类型,我不明白为什么 Guava 团队会有任何不同 :P 现在,说正经的,我认为这是一个错误或者很久以前做出的决定,已经证明不是最好的。 - fps
4个回答

8
toStringFunction() 是一个 Function<Object, String> 而不是 Function<T, String> 的原因非常简单,因为你可以在任何 Object 上调用 toString()。它实际上是一个接受对象并返回字符串的函数。Guava 避免在没有必要的情况下引入类型参数(和通配符泛型)到方法中。
你的示例中的问题在于,当实际上 f1 函数是一个 Function<Object, Double> 时,你试图将其类型定义为 Function<Integer, Double>:它可以接受任何对象,调用 toString() 方法,将该字符串传递给您的 forMap 函数并获取结果。
此外,假设正确使用了泛型,你的函数的类型实际上不必是 Function<Integer, Double>,而是应该像 Function<? super F, ? extends T> 这样被接受的大多数代码所接受。因此,这里真正的问题是:“为什么要将 Function<Object, Double> 称为 Function<Integer, Double>?”

4
我已经花了几个小时在同一个问题上纠结不清,但如果你看一下我的回答评论,你会发现这没有任何进展。问题发起者因为执迷于类型参数具有某些神奇的好处,比如验证,而无法听取理性的解释。然而,在这种情况下,类型参数并没有什么区别。 - tinker
@tinker:没错,完全同意你一直在说的话。只是想用自己的方式来解释一下。 - ColinD
总是很高兴看到经验丰富的用户的答案。我指的是您答案中的最后一行。 - tinker

4

经过评论讨论:

要修复编译错误:

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

您的问题意味着您想要一个`Function `而不是`Function `。根据评论中的讨论,并尝试考虑其他情况,您想要这样做的唯一原因是为了验证。toStringFunction中的代码超出了您的控制,因此在该函数中进行验证是不可能的。至于forMap函数中的验证,该代码也超出了您的控制范围。如果您想防止IllegalArgumentException,则必须对输入进行验证,无论您提供的输入是Object还是Integer。因此,这种验证也超出了forMap,toStringFunction和compose的范围,因为这些函数都不提供这种类型的验证,您必须编写自己的代码来执行此操作(捕获异常并处理它或以某种方式进行预验证在调用此组合函数之前)。
对于特定的组合,通过将Function的输入参数从Object更改为Integer,您不会获得任何好处,因为Function只需要Object中存在的方法,因此在这种情况下这是一个好决定,因为它简化了事情并使其更广泛地可用。假设您能够强制执行参数类型为Integer,则仍然没有任何好处,无论您是否尝试进行IllegalArgumentException的验证或其他任何事情。
在设计API时,您希望尽可能多地使用消费者,而无需费力,即应使用满足您要求的最高级别类。通过在toStringFunction中使用Object,Guava函数正在满足此原则。这使得功能更通用且更广泛可用。这就是选择Object而不是参数化此函数的原因。
如果Function需要参数,则它现在所做的事情与其不同,因此为什么要在不需要的情况下使用参数。这种方法大大简化了设计,而不是添加不需要的内容。
要修复编译错误:Function f1 = Functions.compose(Functions.forMap(someMap),Functions.toStringFunction())。

1
但是指定类型有什么效果呢?在这种情况下,函数是固定的,无论你传递什么类型都没有影响。如果类型很重要,我可以理解这种情况,但是由于每种类型都有toString,所以没有必要区分类型。 - tinker
1
@fasttooth 你明白答案了吗?在这种情况下,擦除甚至不相关。没有理由做你想做的事情。 - tinker
2
@fasttooth 每种类型都是对象,这怎么会是限制呢?如果说有什么的话,那它反而太灵活了。我感觉你偏离了问题的本意。你问为什么要这样做,原因就是让输入类型尽可能地通用,没有比 Object 更通用的了。 - tinker
1
你还没有提供支持你想要做的理性论据。整数是一个对象,由于函数中的代码不受你的控制,所以函数接受什么参数对你来说并不重要。这就像告诉服务所有者更改他们的API,因为你希望它更严格一样。如果服务所有者想要拥有开放的API,那么这是他们的决定,只要不影响消费者。在这种情况下,我看不出参数化会有任何帮助。 - tinker
1
@magnamag 是的,但这种验证超出了toStringFunction的范围。对于在映射中不存在的整数也是同样的情况。我们正在讨论toStringFunction的API设计,任何形式的验证都超出了范围,因为在两个复杂函数组合并使用自定义对象类型的更复杂情况下,需要验证的内容是没有限制的。 - tinker
显示剩余15条评论

4
Guava团队成员在其错误跟踪器中类似问题的评论中解释了为什么Guava没有将其声明为Function<T, String>(尽管可以这样做):
“我们决定库API中类型参数和通配符的目的是确保可以使用任何逻辑上合理的参数类型调用该方法。一旦满足此条件,我们就会停止添加只用于让您“调整”所需结果类型的类型参数/通配符。”
因此,他们基本上认为这不值得。与其更改像toStringFunction这样的方法以返回Function<T, String>,则应更改泛型类型的消费者,使其允许给出Function<? super F, ? extends T>

1

关于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或任何其他运行时错误,因为helper方法是私有的,并且仅用于推断Function类型参数。
如果我强制使用泛型类型,即:
Function<Integer, Double> f1 = Functions.compose(
    Functions.forMap(someMap), 
    Sample.<BigInteger> checkedToStringFunction()); // compilation error

我会收到编译错误。要解决这个问题,我需要更改 f1 函数的第一个类型参数为:

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>。我认为这是一种缩小类型转换,因此我相信它是合法和安全的。如果可以的话,请告诉我是否可以这样做,以及是否值得冒这个风险。


1
那不是“安全”的做法;你是在欺骗编译器将 Function<Object, String> 的实例强转为类型 Function<Integer, String>。这就是所谓的“堆污染”。(而且如果你真的要这么做,直接将其强制转换为原始类型 Function 更简单。没有必要使用帮助方法。) - ruakh
1
在这种情况下,执行此强制转换确实是安全的:它永远不会在运行时导致“ClassCastException”。对于那些不安全的情况,例如将List<Object>强制转换为List<String>,可以使用术语“堆污染”。 - Philipp Wendler
@ruakh 嗯,如果我能做到,那就是合法的。这有点像黑客技巧,但我不认为它是“肮脏”的。我们谈论的是一个强制转换,它只影响参数化类型,对运行时已经没有影响了。我返回的仍然是一个 Function - fps
1
回复:“如果我能做到,那就是合法的”:这在软件界和法庭上同样适用。;-) - ruakh
1
@Magnamag:编译器向您发出了判断:它说无法确定这是否正确。(实际情况是不正确的)。然后,您告诉编译器闭嘴 - 在真正的法庭上您是做不到的,所以在这个意义上是不同的。 :-) - ruakh
显示剩余2条评论

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