【问题标题】:What is the point of allowing type witnesses on all method calls?在所有方法调用中允许类型见证有什么意义?
【发布时间】:2015-01-18 21:12:11
【问题描述】:

假设我们有如下两种方法:

public static <T> T genericReturn() { /*...*/ }
public static String stringReturn() { /*...*/ }

在调用任何方法时,无论是否有任何要求,都可以提供类型见证:

String s;
s = Internet.<String>genericReturn(); //Type witness used in return type, returns String
s = Internet.<Integer>stringReturn(); //Type witness ignored, returns String

但是,我在 Java 中根本没有看到任何实际用途,除非无法推断类型(这通常表明存在更大的问题)。此外,当使用不当时,它会被简单地忽略,这一事实似乎违反直觉。那么在 Java 中使用这个有什么意义呢?

【问题讨论】:

  • 我很惊讶你的第一个 sn-p 甚至编译,因为 get() 不是通用方法。需要进一步研究一下。你在哪里见过这样的代码?也许这是一个奇怪的反编译器的结果?
  • 虽然类型参数无关紧要 - ExampleSuper sup = dataList.&lt;String&gt;get(0); 也可以编译。
  • @JonSkeet get 的返回类型是泛型的,它受类自己定义的泛型修饰符 &lt;E&gt; 的限制
  • 但这并不能使 方法 通用。方法本身没有类型参数;类型参数在 class 上。为没有类型参数的方法指定类型参数对我来说毫无意义。
  • @JonSkeet 经过一些测试,您似乎可以在任何方法调用中添加“参数化”,只要转换是合法的(向上转换)。

标签: java generics


【解决方案1】:

来自JLS §15.2.12.1

  • 如果方法调用包含显式类型参数,并且该成员是泛型方法,则类型参数的数量等于方法的类型参数的数量。

此子句暗示非泛型方法可能适用于提供显式类型参数的调用。事实上,它可能会被证明是适用的。在这种情况下,类型参数将被简单地忽略。

后面是理由

此规则源于兼容性问题和可替代性原则。由于接口或超类可以独立于它们的子类型进行泛型,我们可以用非泛型方法覆盖泛型方法。但是,覆盖(非泛型)方法必须适用于对泛型方法的调用,包括显式传递类型参数的调用。否则,子类型将无法替代其泛型的超类型。

按照这个推理,让我们构建一个例子。假设在 Java 1.4 中,JDK 有一个类

public class Foo
{
    /** check obj, and return it */
    public Object check(Object obj){ ... }
}

一些用户编写了一个扩展Foo 并覆盖check 方法的专有类

public class MyFoo extends Foo
{
    public Object check(Object obj){ ... }
}

Java 1.5 引入泛型时,Foo.check 被泛化为

    public <T> T check(T obj)

雄心勃勃的向后可比性目标要求MyFoo 仍可在 Java 1.5 中编译而无需修改;而MyFoo.check[Object-&gt;Object] 仍然是Foo.check[T-&gt;T] 的覆盖方法。

现在,根据上述理由,因为这样编译:

    MyFoo myFoo = new MyFoo();

    ((Foo)myFoo).<String>check("");

这也必须编译:

    myFoo.<String>check("");

即使MyFoo.check 不是通用的。


这听起来有点牵强。但即使我们相信这个论点,解决方案仍然过于广泛和过度。 JLS 可以收紧它,以便 myFoo.&lt;String,String&gt;checkobj.&lt;Blah&gt;toString() 是非法的,因为类型参数数量不匹配。他们可能没有时间解决它,所以他们只是采取了一条简单的路线。

【讨论】:

  • 这解释了为什么它是有效的(尽管不是为什么这种语言是这样设计的——这很奇怪,IMO),但它没有解释为什么有人会使用它。我的理解是,这是问题的主旨。
  • 我注意到我对更新问题的测试忽略了,呵呵。但或多或少,为什么它存在?
  • 允许这样做的原因可能类似于为什么可以在实例上调用静态方法。它允许现有代码在方法未生成后继续编译。
  • @MarkoTopolnik:或者,可以允许代码在存在时利用方法的泛型版本,同时也可以与非泛型方法一起使用。这在涉及继承的场景中,或者在某些代码的使用者可能将其与不同版本的库一起使用的场景中可能会有所帮助(其中一些会使用泛型,而另一些则不会)。
  • @supercat 我已将我的答案标记为社区 wiki。请随时编辑答案以反映您的评论。
【解决方案2】:

当类型推断不起作用时,您需要类型见证(菱形中的类型)(请参阅http://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html

为此给出的示例是菊花链调用如下:

processStringList(Collections.emptyList());

其中 processStringList 定义为:

void processStringList(List<String> stringList) 
{
    // process stringList
}

这将导致错误,因为它无法将 List&lt;Object&gt; 转换为 List&lt;String&gt;。因此,需要见证人。不过,您可以分多个步骤执行此操作,但这会方便得多。

【讨论】:

  • emptyList 是泛型的,但在非泛型方法上甚至允许使用类型参数,这有点奇怪。
  • 对,这就是“类型见证”设计的用例。问题是为什么它也允许用于非泛型方法。在原始帖子的示例中,这将是 stringReturn 方法。
  • 这在 Java SE 8 中不再需要。来自同一链接。
【解决方案3】:

这是因为向后和/或向前兼容(在源代码级别)。

想象一下,在 JDK 7 的 Swing 中为某些类引入了泛型参数。这也可能发生在方法上(即使有限制)。如果结果证明是个坏主意,您可以删除它们,并且使用它的源代码仍然可以编译。在我看来,这就是允许这样做的原因,即使它没有被使用。

虽然灵活性有限。如果您引入了带有n 类型的类型参数,则以后不能在m != 0m != n 时更改为m 类型(以与源代码兼容的方式)。

(我知道这可能无法回答您的问题,因为我不是 Java 的设计者,这只是我的想法/意见。)

【讨论】:

  • 这听起来不对。从来没有一个目标是保持非泛化的可比性。
【解决方案4】:

想知道为什么 Java 中会出现“类型见证”吗? :D

要理解这一点,我们应该从Type Inference开始故事。

类型推断是 Java 编译器查看每个方法调用和相应声明以确定使调用适用的类型参数(或参数)的能力。 推理算法确定参数的类型,如果可用,还确定分配或返回结果的类型。最后,推理算法尝试找到适用于所有参数的最具体的类型。

如果上述算法仍然无法确定类型我们有“类型见证”来明确说明我们需要什么类型。例如:

public class TypeWitnessTest {

    public static void main(String[] args) {
        print(Collections.emptyList());
    }

    static void print(List<String> list) {
        System.out.println(list);
    }
}

以上代码无法编译:

TypeWitnessTest.java:11: error: method print in class TypeWitnessTest cannot be applied to given types;
            print(Collections.emptyList());
            ^
  required: List<String>
  found: List<Object>
  reason: actual argument List<Object> cannot be converted to List<String> by method invocation conversion
1 error

所以,你有 Type Witness 来解决这个问题:

public class TypeWitnessTest {

    public static void main(String[] args) {
        print(Collections.<String>emptyList());
    }

    static void print(List<String> list) {
        System.out.println(list);
    }
}

这是可编译的并且工作正常,但是在Java 8中得到了更多改进:
JEP 101: Generalized Target-Type Inference

PS:我从基础开始,以便其他 StackOverflow 读者也能从中受益。

编辑

在非通用见证上键入见证

public class InternetTest {
    public static void main(String[] args) {
        String s;
        s = Internet.<String>genericReturn(); //Witness used in return type, returns a string
        s = Internet.<Integer>stringReturn(); //Witness ignored, returns a string
    }
}

class Internet {
    public static <T> T genericReturn() { return null; }
    public static String stringReturn() { return null; }
}

我尝试使用javac 1.6.0_65 模拟@Rogue 示例,但编译失败并出现以下错误:

javac InternetTest.java 
InternetTest.java:5: stringReturn() in Internet cannot be applied to <java.lang.Integer>()
        s = Internet.<Integer>stringReturn(); //Witness ignored, returns a string
                    ^
1 error

@Rogue:如果您使用的版本比我使用的要早,请告诉我您的 javac 版本。如果你当时是,现在是不允许的。 :P

【讨论】:

  • 对,这就是“类型见证”设计的用例。问题是为什么 non-generic 方法也允许它。在原始帖子的示例中,这将是 stringReturn 方法。
  • @asteri:在非泛型方法上键入 Witness!当我尝试它时,它从未使用 javac 版本 1.6.0_65 编译
  • 你需要使用JDK 7。
  • Internet.&lt;Integer&gt;stringReturn() 中的显式类型参数在 java 6 中也应该被忽略。我认为这是编译器的错误。
猜你喜欢
  • 2013-11-12
  • 1970-01-01
  • 2021-07-20
  • 1970-01-01
  • 1970-01-01
  • 2021-03-21
  • 1970-01-01
  • 2023-04-05
相关资源
最近更新 更多