通用参数:似乎只有金刚石运算符可以工作

2022-09-03 06:34:39

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

假设我们有以下代码:

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

它可以在没有警告的情况下编译并执行良好。但是,如果试图以某种方式显式定义 in 的推断类型,编译器就会抱怨。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>();
                   ^

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

答案 1

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<?>>


答案 2

使用通配符实例化泛型

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

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

不可能显式实例化 a(或 a)。在实例化泛型类型时,通配符不能用作类型参数,尽管它们可能嵌套在类型参数。例如,虽然实例化 是合法的,但您无法实例化 .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#1Sample<?> s = get0()

类型签名中的多个通配符

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

假设返回类型的值 。如果您打算确保扩展,那么您就失败了。考虑:get()XXSample<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 (!)
    }
}

在 中,变量保存的值是 a 和 a,但不是 。这是你的本意吗?如果没有,我建议将方法签名中的通配符替换为类型变量,然后让调用方决定类型参数:mainsSample<X>YSample<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
    }
}

上面的版本有效地保证了,对于类型的某个返回值,返回的值扩展。从理论上讲,它甚至在绑定到通配符时也有效。为什么?它返回到通配符捕获ASample<A>T

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

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


旁白

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


推荐