通用参数:只有菱形运算符似乎起作用

时间:2018-06-19 20:17:53

标签: java generics type-inference

背景:问题在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();
    }
}

它编译时没有警告,并且执行得很好。但是,如果尝试以某种方式在return new Sample<>();中定义get()的推断类型,则编译器会抱怨。

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

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


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>();
                   ^

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

return new Sample<Sample<>>();导致:

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

2 个答案:

答案 0 :(得分:11)

JLS只是说:

  

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

因此,是否存在一些可以满足解决方案的推断X是的。

当然,要让您明确定义这样的X,您必须声明它:

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

显式Sample<X>与返回类型Sample<? extends Sample<?>>兼容,因此编译器很满意。

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

答案 1 :(得分:8)

使用通配符实例化泛型

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

  

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

不可能显式实例化Sample<? extends Sample<?>>(或为此事例Sample<?>)。实例化泛型类型时,通配符不能用作类型参数,尽管通配符可能嵌套在 类型参数内。例如,虽然实例化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();
    }
}

但是,如果您特别想返回包含通配符的Sample<>类的泛型实例化,则必须依靠泛型推断来为您确定类型参数。有几种解决方法,但是通常涉及以下之一:

  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<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 (!)
    }
}

main中,变量s的值是Sample<X>Y,但不是一个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
    }
}

以上版本有效地保证,对于类型为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。这是因为 type erasure :编译器可以发出一条classcast指令来验证返回的值是XSample,但它无法验证其实际上是Sample<X>,因为Sample的所有通用形式都具有相同的运行时类型。对于具体的类型参数,编译器通常可以阻止可疑代码的编译,但是当您将通配符放入组合中时,复杂的通用约束变得难以执行或无法执行。买方当心:)。


在旁边

如果这一切使您感到困惑,请不要担心:通配符和通配符捕获是Java泛型最难理解的方面之一。还不清楚了解这些内容是否会真正帮助您实现近期目标。如果您打算使用API​​,则最好将其提交给“代码审查”堆栈交换,并查看您将获得什么样的反馈。