【问题标题】:Why use a wild card capture helper method?为什么要使用通配符捕获辅助方法?
【发布时间】:2015-06-10 18:00:07
【问题描述】:

参考:Wildcard Capture Helper Methods

它说要创建一个辅助方法来捕获通配符。

public void foo(List<?> i) {
    fooHelper(i);
}        
private <T> void fooHelper(List<T> l) {
    l.set(0, l.get(0));
}

仅使用下面的这个函数不会产生任何编译错误,而且似乎工作方式相同。我不明白的是:你为什么不直接使用它并避免使用助手?

public <T> void foo(List<T> l) {
    l.set(0, l.get(0));
}

我认为这个问题真的可以归结为:通配符和泛型有什么区别?所以,我去了这个:difference between wildcard and generics。 它说使用类型参数:

1) 如果你想在不同类型的方法参数上强制执行某种关系,你不能用通配符来做到这一点,你必须使用类型参数。

但是,这不正是带有辅助函数的通配符实际上在做什么吗?它不是在不同类型的方法参数与其设置和获取未知值之间强制建立关系吗?

我的问题是:如果你必须定义一些需要在不同类型的方法参数上建立关系的东西,那么为什么首先使用通配符,然后使用辅助函数呢?

这似乎是一种合并通配符的 hacky 方式。

【问题讨论】:

  • 不,我不是已经看过了

标签: java generics wildcard


【解决方案1】:

在这种特殊情况下,这是因为List.set(int, E) 方法要求类型与列表中的类型相同。

如果您没有辅助方法,编译器不知道? 是否与List&lt;?&gt;get(int) 的返回相同,因此您会收到编译器错误:

The method set(int, capture#1-of ?) in the type List<capture#1-of ?> is not applicable for the arguments (int, capture#2-of ?)

用helper方法,你是在告诉编译器,类型是一样的,只是不知道是什么类型。

那么为什么要有非辅助方法呢?

直到 Java 5 才引入泛型,因此有很多代码早于泛型。 Java 5 之前的 List 现在是 List&lt;?&gt;,因此如果您尝试在通用感知编译器中编译旧代码,如果您无法更改方法签名,则必须添加这些辅助方法。

【讨论】:

  • 对,但是如果你忽略通配符函数,只使用助手你仍然可以得到正确的输出而不会出现任何编译错误。
  • @BrandonLing 是的,我会更新我的答案。
  • 我明白为什么编译器不理解没有辅助函数的通配符,但 pre-java5 部分回答了我的问题。谢谢。
  • 这个答案解释了我无法理解的问题的两个方面,即为什么需要辅助方法以及辅助方法如何解决编译时错误。在此之前我发现它很混乱。我想为什么不首先编写一个通用方法并完成它。谢谢。
【解决方案2】:

我同意:删除辅助方法并键入公共 API。没有理由不这样做,而且完全有理由这样做。

总结一下通配符版本对助手的需求:虽然这对我们人类来说是显而易见的,但编译器并不知道从l.get(0) 返回的未知类型是相同未知的列表本身的类型。即它没有考虑到set()调用的参数来自与目标相同的列表对象,所以它必须是一个安全的操作。它只注意到get()返回的类型未知,目标列表的类型未知,两个未知数不保证是同一类型。

【讨论】:

    【解决方案3】:

    您说得对,我们不必使用通配符版本。

    归结为哪个 API 看起来/感觉“更好”,这是主观的

        void foo(List<?> i) 
    <T> void foo(List<T> i)
    

    我会说第一个版本更好。

    如果有界限

        void foo(List<? extends Number> i) 
    <T extends Number> void foo(List<T> i)
    

    第一个版本看起来更加紧凑;类型信息都在一个地方。

    此时,通配符版本是惯用的方式,程序员更熟悉。

    JDK 方法定义中有很多通配符,尤其是在 java8 引入 lambda/Stream 之后。诚然,它们非常丑陋,因为我们没有方差类型。但是想想如果我们将所有通配符都扩展为类型 var 会更难看。

    【讨论】:

    • @SotiriosDelimanolis - 我从问题中得到了不同的解读 - OP 询问 &lt;?&gt; 版本的意义何在,而 &lt;T&gt; 版本工作得很好。
    • OPs 知道&lt;?&gt; 在调用方工作;但实施者方面需要一个&lt;T&gt; 助手。 OP 认为&lt;?&gt; 版本是多余的中间人,问我们为什么不直接公开&lt;T&gt; 版本。
    【解决方案4】:

    Java 14 Language Specification, Section 5.1.10 (PDF) 用一些段落解释了为什么人们更愿意提供通配符方法publicly,同时使用通用方法privately。具体来说,他们说(public 泛型方法):

    这是不可取的,因为它会将实现信息暴露给调用者。

    这是什么意思?究竟是什么暴露在一个而不是另一个?

    您知道可以将类型参数直接传递给方法吗?如果您在Foo 类上有一个静态方法&lt;T&gt; Foo&lt;T&gt; create()——是的,这对我来说对静态工厂方法最有用——那么您可以将它调用为Foo.&lt;String&gt;create()。您通常不需要 -- 或 想要 -- 来执行此操作,因为 Java 有时可以从任何提供的参数中推断出这些类型。但事实仍然是您可以显式提供这些类型。

    所以泛型&lt;T&gt; void foo(List&lt;T&gt; i) 在语言级别实际上接受了两个参数:列表的元素类型和列表本身。我们修改了方法契约,只是为了在实现方面节省一些时间!

    很容易认为&lt;?&gt; 只是更明确的通用语法的简写,但我认为Java 的符号实际上掩盖了这里真正发生的事情。让我们翻译一下类型论的语言:

    /*                 Java *//* Type theory      */
                 List<?>     ~~   ∃T. List<T>
        void foo(List<?> l)  ~~  (∃T. List<T>) -> ()
    <T> void foo(List<T> l)  ~~   ∀T.(List<T>  -> ()
    

    List&lt;?&gt; 这样的类型称为存在类型? 意味着有某种类型可以去那里,但我们不知道它是什么。在类型论方面,∃T. 的意思是“存在一些 T”,这基本上就是我在上一句中所说的——我们刚刚给那个类型起了一个名字,尽管我们仍然不知道它是什么.

    在类型论中,函数的类型为A -&gt; B,其中A 是输入类型,B 是返回类型。 (出于愚蠢的原因,我们将void 写为()。)请注意,在第二行,我们的输入类型与我们一直在讨论的存在列表相同。

    第三行发生了一些奇怪的事情!在 Java 方面,看起来我们只是简单地命名了通配符(这对它来说并不是一个糟糕的直觉)。在类型理论方面,我们已经说过_表面上与上一行非常相似:对于调用者选择的任何类型,我们将接受 该类型的列表。 (∀T. 实际上读作“对于所有 T”。)但是 T范围 现在完全不同了——括号已移动以包含输出类型!这很关键:如果没有更广泛的范围,我们就无法编写像 &lt;T&gt; List&lt;T&gt; reverse(List&lt;T&gt; l) 这样的东西。

    但是如果我们不需要更广泛的范围来描述函数的契约,那么缩小变量的范围(是的,甚至是类型级别的变量)可以更容易地推理这些变量.该方法的存在形式让调用者非常清楚,列表元素类型的相关性不会超出列表本身。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-23
      • 2020-06-06
      • 2020-05-17
      • 2011-09-22
      相关资源
      最近更新 更多