通用参数:只有菱形运算符似乎可以工作

Tur*_*g85 16 java generics type-inference

背景:这个问题出现了这个问题(答案的第一个修订版,确切地说).此问题中提供的代码被简化为最低限度来解释问题.

假设我们有以下代码:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

它在没有警告的情况下编译并执行正常.然而,如果试图以某种方式定义推断类型return new Sample<>();get()明确编译器会抱怨.

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

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


return new Sample<Sample> 结果是:

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>();
                   ^
Run Code Online (Sandbox Code Playgroud)

return new Sample<Sample<?>> 结果是:

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
Run Code Online (Sandbox Code Playgroud)

return new Sample<Sample<>>(); 结果是:

Sample.java:6: error: illegal start of type
           return new Sample<Sample<>>();
                                    ^
Run Code Online (Sandbox Code Playgroud)

And*_*eas 11

JLS只是说:

如果类的类型参数列表为空(菱形形式<>),则推断类的类型参数.

那么,有一些推断X会满足解决方案吗?是.

当然,为了明确定义这样的X,你必须声明它:

public static <X extends Sample<X>> Sample<? extends Sample<?>> get() {
    return new Sample<X>();
}
Run Code Online (Sandbox Code Playgroud)

explicit Sample<X>与返回类型兼容Sample<? extends Sample<?>>,因此编译器很高兴.

返回类型混乱的事实Sample<? extends Sample<?>>是一个完全不同的故事.


Mik*_*bel 8

使用通配符实例化泛型

这里有几个问题,但在深入研究之前,让我解决一下你的实际问题:

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

不可能明确地实例化Sample<? extends Sample<?>>(或者Sample<?>就此而言).在实例化泛型类型时,通配符不能用作类型参数,尽管它们可能嵌套类型参数中.例如,虽然实例化a是合法的ArrayList<Sample<?>>,但是你无法实例化ArrayList<?>.

最明显的解决方法是简单地返回一些可分配的 其他具体类型Sample<?>.例如:

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

    public static Sample<? extends Sample<?>> get() {
        return new X();
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您特别想要返回Sample<>包含通配符的类的通用实例化,那么您必须依赖通用推理来为您计算类型参数.有几种方法可以解决这个问题,但通常涉及以下方面之一:

  1. 使用钻石操作员,正如您现在所做的那样.
  2. 委托使用类型变量捕获通配符的泛型方法.

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

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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

这里,返回类型Sample<T> get0()被扩展为Sample<WC#1 extends Sample<WC#1>>,其中WC#1表示从赋值目标中推断出的通配符的捕获副本Sample<?> s = get0().

类型签名中的多个通配符

现在,让我们解决你的方法签名.很难根据您提供什么样的代码告诉是肯定的,但我是的返回类型Sample<? extends Sample<?>>*不*你真正想要的.当通配符出现在某个类型中时,每个通配符都与其他通配符不同.没有强制执行第一个通配符和第二个通配符引用相同的类型.

假设get()返回一个类型的值X.如果您打算确保X延伸Sample<X>,那么您就失败了.考虑:

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 (!)
    }
}
Run Code Online (Sandbox Code Playgroud)

main,变量s包含a Sample<X>和a 的值Y,但不是 a Sample<Y>.这是你的意图吗?如果没有,我建议使用类型变量替换方法签名中的通配符,然后让调用者决定类型参数:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的版本有效地保证,对于某些类型A的返回值,返回的值会扩展Sample<A>.理论上,它甚至可以在T绑定到通配符时起作用.为什么?它可以追溯到通配符捕获:

在您的原始get方法中,两个通配符最终可能会引用不同的类型.实际上,您的返回类型是Sample<WC#1 extends Sample<WC#2>,在哪里WC#1WC#2是以任何方式无关的单独通配符.但在上面的示例中,绑定T到通配符会捕获它,允许相同的通配符出现在多个位置.因此,当T绑定到通配符时WC#1,返回类型将扩展为Sample<WC#1 extends Sample<WC#1>.请记住,没有办法直接在Java中表达该类型:它只能通过依赖类型推断来完成.

现在,我说这在理论上适用于通配符.实际上,您可能无法以get通用约束可运行时强制执行的方式实现.这是因为类型擦除:编译器可以发出classcast指令来验证返回的值是,例如,a X和a Sample,但它无法验证它实际上是a Sample<X>,因为所有通用形式Sample具有相同的运行时类型.对于具体类型参数,编译器通常可以防止可疑代码编译,但是当您将通配符放入混合时,复杂的通用约束变得很难或不可能实施.买家要小心:).


在旁边

如果所有这些让您感到困惑,请不要担心:通配符和通配符捕获是Java泛型需要理解的最困难的方面.目前还不清楚理解这些是否真的会帮助您实现目标.如果您有API,最好将其提交给Code Review堆栈交换,看看您获得了什么样的反馈.