【问题标题】:Generic parameter: only diamond operator seems to work通用参数:似乎只有菱形运算符有效
【发布时间】:2018-06-19 20:17:53
【问题描述】:

背景:问题出现在this answer(确切地说,是答案的第一个修订版)。此问题中提供的代码已简化为解释问题的最低限度。

假设我们有以下代码:

public class Sample<T extends Sample<T>> {

    public static Sample<? extends Sample<?>> get() {
        return new Sample<>();
    }

    public static void main(String... args) {
        Sample<? extends Sample<?>> sample = Sample.get();
    }
}

它在没有警告的情况下编译并且执行良好。但是,如果有人试图以某种方式在get() 中明确定义return new Sample&lt;&gt;(); 的推断类型,编译器会抱怨。

到目前为止,我的印象是菱形运算符只是一些语法糖,不能编写显式类型,因此总是可以用一些显式类型替换。对于给定的示例,我无法为返回值定义任何显式类型以使代码编译。是否可以显式定义返回值的泛型类型或者在这种情况下是否需要菱形运算符?

下面是我尝试用相应的编译器错误显式定义返回值的泛型类型。


return new Sample&lt;Sample&gt; 结果:

Sample.java:6: error: type argument Sample is not within bounds of type-variable T
            return new Sample<Sample>();
                              ^
  where T is a type-variable:
    T extends Sample<T> declared in class Sample
Sample.java:6: error: incompatible types: Sample<Sample> cannot be converted to Sample<? extends Sample<?>>
            return new Sample<Sample>();
                   ^

return new Sample&lt;Sample&lt;?&gt;&gt; 结果:

Sample.java:6: error: type argument Sample<?> is not within bounds of type-variable T
            return new Sample<Sample<?>>();
                                    ^
  where T is a type-variable:
    T extends Sample<T> declared in class Sample

return new Sample&lt;Sample&lt;&gt;&gt;(); 结果:

Sample.java:6: error: illegal start of type
           return new Sample<Sample<>>();
                                    ^

【问题讨论】:

  • 您写public static Sample&lt;? extends Sample&lt;?&gt;&gt; get() 而不仅仅是public static Sample&lt;?&gt; get() 有什么特别的原因吗?在我看来,您已经以类型参数必须扩展 Sample 自身的方式定义了 Sample。该声明对我来说似乎是多余的,如果您无法使两个 ? 值匹配,可能会导致问题。
  • @DawoodibnKareem 看看上面提到的答案(及其问题)。它是在编写构建器和在构建器之间使用继承的背景下出现的。
  • 不,我觉得链接的问题中没有任何内容可以证明将 extends 子句添加到该方法签名中是合理的。尝试使 get 返回一个 Sample&lt;?&gt;
  • @DawoodibnKareem 忘记我刚刚写的内容。这是一个脑放屁。对于具体问题,您似乎是正确的。我的问题仍然存在:这是需要&lt;&gt; 的情况吗?
  • 我认为Sample&lt;?&gt;Sample&lt;? extends Sample&lt;?&gt;&gt; 是等价的类型。参见例如ideone.com/XkOkZf。不过,我想不出一种直接的方式来解释这里发生了什么。

标签: java generics type-inference


【解决方案1】:

JLS 简单地说:

如果类的类型参数列表为空——菱形形式&lt;&gt;——类的类型参数推断

那么,是否有一些推断的X 可以满足解决方案? 是的。

当然,要显式定义这样的X,你必须声明它:

public static <X extends Sample<X>> Sample<? extends Sample<?>> get() {
    return new Sample<X>();
}

显式Sample&lt;X&gt; 与返回类型Sample&lt;? extends Sample&lt;?&gt;&gt; 兼容,因此编译器很高兴。

返回类型是一团糟Sample&lt;? extends Sample&lt;?&gt;&gt; 的事实完全不同。

【讨论】:

  • 像往常一样:如果您看到解决方案,那是显而易见的。很好的答案。
  • 这似乎是一个相当奇怪的签名。为什么不直接写public static &lt;X extends Sample&lt;X&gt;&gt; Sample&lt;X&gt; get(),让调用者决定X是绑定通配符还是具体类型?
  • @MikeStrobel 询问oleg.cherednik。正是他提出了触发 this 问题的疯狂代码。我只是在回答这里发布的钻石操作员问题。
  • @MikeStrobel 这实际上会导致编译时错误,因为builder()-方法(在AB 中)将具有相同的擦除。 “最干净”的解决方案似乎是返回Builder&lt;?&gt;
【解决方案2】:

使用通配符实例化泛型

这里有几个问题,但在深入研究它们之前,让我先解决您的实际问题:

是否可以显式定义返回值的通用类型,或者在这种情况下是否需要菱形运算符?

不可能显式实例化Sample&lt;? extends Sample&lt;?&gt;&gt;(或Sample&lt;?&gt;)。在实例化泛型类型时,通配符不得用作类型参数,尽管它们可能嵌套在类型参数中。例如,虽然实例化 ArrayList&lt;Sample&lt;?&gt;&gt; 是合法的,但您不能实例化 ArrayList&lt;?&gt;

最明显的解决方法是简单地返回一些其他具体类型,可分配给Sample&lt;?&gt;。例如:

class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}

    public static Sample<? extends Sample<?>> get() {
        return new X();
    }
}

但是,如果您特别想要返回包含通配符的 Sample&lt;&gt; 类的通用实例化,那么您必须依靠通用推理来为您计算出类型参数。有几种方法可以解决此问题,但通常涉及以下之一:

  1. 使用菱形运算符,就像您现在所做的那样。
  2. 委派给一个泛型方法,该方法使用类型变量捕获通配符。

虽然您不能在泛型实例化中直接包含通配符,但包含类型变量是完全合法的,这就是使选项 (2) 成为可能的原因。我们所要做的就是确保委托方法中的类型变量在调用站点绑定到通配符。每次提到类型变量,方法的签名和主体都会被替换为对该通配符的引用。例如:

public class Sample<T extends Sample<T>> {
    public static Sample<? extends Sample<?>> get() {
        final Sample<?> s = get0();
        return s;
    }

    private static <T extends Sample<T>> Sample<T> get0() {
        return new Sample<T>();
    }
}

这里,Sample&lt;T&gt; get0() 的返回类型扩展为Sample&lt;WC#1 extends Sample&lt;WC#1&gt;&gt;,其中WC#1 表示从Sample&lt;?&gt; s = get0() 中的分配目标推断出的通配符的捕获副本。

类型签名中的多个通配符

现在,让我们解决您的方法签名。根据您提供的代码,很难确定,但我猜测 Sample&lt;? extends Sample&lt;?&gt;&gt; 的返回类型 *不是* 您真正想要的。当通配符出现在一个类型中时,每个通配符与其他通配符不同。没有强制要求第一个通配符和第二个通配符引用相同的类型。

假设get() 返回一个X 类型的值。如果您打算确保X 扩展Sample&lt;X&gt;,那么您就失败了。考虑:

class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}
    static class Y extends Sample<X> {}

    public static Sample<? extends Sample<?>> get() {
        return new Y();
    }

    public static void main(String... args) {
        Sample<?> s = Sample.get(); // legal (!)
    }
}

main 中,变量s 的值是Sample&lt;X&gt;Y,但不是 Sample&lt;Y&gt;。那是你的本意吗?如果不是,我建议用类型变量替换方法签名中的通配符,然后让调用者决定类型参数:

class Sample<T extends Sample<T>> {
    static class X extends Sample<X> {}
    static class Y extends Sample<X> {}

    public static <T extends Sample<T>> Sample<T> get() { /* ... */ }

    public static void main(String... args) {
        Sample<X> x = Sample.get();     // legal
        Sample<Y> y = Sample.get();     // NOT legal

        Sample<?> ww = Sample.get();    // legal
        Sample<?> wx = Sample.<X>get(); // legal
        Sample<?> wy = Sample.<Y>get(); // NOT legal
    }
}

上述版本有效地保证,对于A 类型的某些返回值,返回值扩展Sample&lt;A&gt;。理论上,当T 绑定到通配符时,它甚至可以工作。为什么?它回到通配符捕获

在您原来的get 方法中,这两个通配符最终可能指代不同的类型。实际上,您的返回类型是Sample&lt;WC#1 extends Sample&lt;WC#2&gt;,其中WC#1WC#2 是单独的通配符,它​​们没有任何关系。但在上面的示例中,将T 绑定到通配符捕获它,允许同一个通配符 出现在多个位置。因此,当T 绑定到通配符WC#1 时,返回类型扩展为Sample&lt;WC#1 extends Sample&lt;WC#1&gt;。请记住,没有办法直接在 Java 中表达该类型:只能依靠类型推断来完成。

现在,我说这适用于通配符理论上。在实践中,您可能无法以通用约束可在运行时强制执行的方式实现get。这是因为类型擦除:编译器可以发出classcast 指令来验证返回值是否为XSample,但它不能 验证它实际上是Sample&lt;X&gt;,因为Sample 的所有通用形式都具有相同的运行时类型。对于具体的类型参数,编译器通常可以阻止可疑代码编译,但是当您将通配符混入其中时,复杂的泛型约束变得难以或不可能强制执行。买家小心:)。


一边

如果您对这一切感到困惑,请不要担心:通配符和通配符捕获是 Java 泛型最难理解的方面之一。也不清楚理解这些是否真的能帮助你实现你的直接目标。如果您有一个 API,最好将其提交给 Code Review 堆栈交换,看看您会得到什么样的反馈。

【讨论】:

  • 这似乎是明智的。但我的问题仍然存在:这是不是通过 Java 中的显式类型无法表达的东西,因此迫使我们使用菱形??
  • 不,你不能直接表达这个类型。您必须依靠类型推断,要么通过菱形运算符,要么通过调用泛型方法并让通配符被类型变量捕获。
猜你喜欢
  • 1970-01-01
  • 2014-01-02
  • 2015-07-14
  • 1970-01-01
  • 2013-08-06
  • 1970-01-01
  • 2012-01-24
  • 2013-01-07
相关资源
最近更新 更多