【问题标题】:Why is this assignment involving wildcards legal in Java?为什么这个涉及通配符的赋值在 Java 中是合法的?
【发布时间】:2023-03-10 22:21:01
【问题描述】:

大多数关于通配符的问题都想知道为什么一些合理的东西会被编译器拒绝。我的问题是相反的。为什么下面的程序会被编译器接受?

void test(List<? extends Number> g1, List<? extends Number> g2)
{
    g1 = g2;
}

我试图从 Java 语言规范中解释这一点,但我没有找到答案。我从对 Java 泛型和通配符的各种描述中得到的印象是,通配符的每次使用都被捕获为一种全新的类型,但显然不是在这里。我没有发现允许此分配后的任何令人讨厌的行为,但它似乎仍然“错误”。

【问题讨论】:

    标签: java generics types wildcard bounded-wildcard


    【解决方案1】:

    当我面对这些问题时,我的处理方式略有不同。

    首先,每个wildcard 都是capturedeverywherejavac。用简单的英语:每次javac“看到”wildcard 都会改变它(这几乎准确,您将进一步看到)。具体来说,假设我们有这个:

    List<? extends Number> list;
    

    javac 将转换为:

    List<X1> list
    

    其中X1 &lt;: Number,其中&lt;: 表示它是的子类型,例如:X1 is an unknown type that extends Number。每次发生都会发生这种情况。在某些情况下,起初可能会很奇怪:

    public static void main(String[] args) {
        List<?> l = new ArrayList<String>();
        one(l);
        two(l, l); // fails
    }
    
    public static <T> void one(List<T> single){
    
    }
    
    public static <T> void two(List<T> left, List<T> right){
    
    }
    

    捕获转换分别应用于每个List,就像发生了这样的事情:

    two(List<X1>, List<X2>)
    

    现在为什么你的例子被接受了,更有趣的是,恕我直言。你知道应用了捕获转换,但是根据JLS它是not applied everywhere

    如果表达式名称是出现在“左侧”的变量,则其类型不受捕获转换的影响。

    这就像说只有被捕获转换,而不是变量

    所以在这种情况下:

    g1 = g2;
    

    g1 尚未进行捕获转换,而 g2 已转换。这就像在做:

    List<? extends Number> g1 = List<X1> (g2) // pseudo-code
    

    我们知道X1 &lt;: Number 所以,List&lt;X1&gt;List&lt;? extends Number&gt; 的子类型,所以分配有效。

    即使您将 ? extends Number 更改为 ?(这不再是有界通配符),这仍然有效。

    【讨论】:

    • 非常有趣。在某些情况下,捕获转换似乎是强制性的,这与根据需要应用的其他转换不同。 JLS 在这一点上不是很清楚。
    【解决方案2】:

    List&lt;? extends Number&gt; 最好读作:

    这是一个数字列表,但是是协变的。

    换句话说,这是一个具体但未知类型的列表。但是,我确实知道,无论它是什么类型,至少它是 Number 或其某个子类。

    泛型很奇怪;一旦你选择了一些差异,你就会得到随之而来的限制。在集合的情况下,“协方差”伴随着“不添加”的包袱。

    试试看。

    g1.add(XXX);

    这里唯一对XXX 合法的东西? null。字面意思就是这样。你可以添加到这个东西的完整、完整和详尽的列表。当然 Number x = 5; g1.add(x); 不会在这里被 javac 允许。

    List&lt;? extends a thingie&gt; 你是在说:是的,我想要那个。我正在签署这个限制,我绝对不会添加任何内容(除了文字 null 的学术案例)。作为给自己戴上手铐的交易,你可以传递给g1的东西大大增加了。

    您也可以选择逆变:

    void foo(List<? super Integer> list) {
        list.add(Integer.valueOf(5)); // works!
        Integer x = list.get(0); // no go
    }
    

    逆变是相反的。添加作品。得到不起作用。在这种情况下,这意味着:表达式list.get(0) 的类型只是.. Object


    现在我们已经介绍了:

    void test(List<? extends Number> g1, List<? extends Number> g2) {}
    

    表示“我的第一个参数是一个数字列表,但我选择了协方差手铐”,“我的第二个参数是一个数字列表,但我也为此选择了协方差手铐”,现在有意义了为什么java让你写g1 = g2。 g2 保证是 X&lt;Y&gt;,其中 X 是 List 的某个具体子类,Y 是 Number 或其某个子类。

    这是 100% 兼容的,类型方面的,与“某种列表,其类型参数是数字的某种协变形式”的概念。您唯一可以做的事情 List&lt;? extends Number&gt; 是调用 List 的方法,其中签名中的任何 T 对参数都“禁用”,并由绑定替换(Number) 用于返回类型。

    这正是List&lt;? extends Number&gt; 所描述的内容,所以它是兼容的。

    【讨论】:

    • tl;dr 分配是有效的,但你会受到下游限制。
    • 恕我直言,这是完全正确的理论,但忽略了问题本身。这与 capture 转换 以及它如何(和不)应用于 variablesvalues 有关。
    • @Eugene 很好,它解释了捕获转换背后的原因。目的是,一旦你理解了泛型背后的逻辑,你就可以用它来“猜测”java 的语言规范所说的内容,并且几乎每次都正确。要么就是这样,要么记住整个 JLS:P
    • @rzwitserloot 我明白你的意思,谢谢跟进。
    【解决方案3】:

    “我从对 Java 泛型和通配符的各种描述中得到的印象是,通配符的每次使用都被视为一种全新的类型,”

    这个说法是正确的。

    那又怎样?您将对象的类型与变量的类型混淆了。

    考虑这段代码:

    String s = "abc";
    Object o = s;
    

    o 的类型为 Object,它与 s 的类型兼容。但这并不意味着 String 和 Object 是同一类型。你的例子没有什么不同。对象有两种不同的 List 类型,但变量有一种类型。每个变量都有类型 List extends Number>,所以分配很好。当您进行分配时,对象的泛型类型是 List 用于一些全新的未知类型 x。但是变量类型仍然是 List

    【讨论】:

    【解决方案4】:

    它怎么可能有效?

    两个变量具有相同的类型(在本例中为 List&lt;? extends Number&gt;),因此编译器必须允许将一个变量赋值给另一个变量。


    赋值给变量的对象可能有不同的类型,但是变量的类型是相同的,所以赋值总是合法的。

    编译器不知道也不关心分配给变量的对象的实际类型是什么,即使它可以从代码中确定。在检查类型时,它只关心声明的类型。

    【讨论】:

    • 但是这些不是相同的类型,每个通配符(无论是否有界)都不同,原因是different
    • @Eugene 不正确:objects 可能是不同的类型,但 variable 类型是相同的,因此赋值始终是合法的。编译器不知道也不关心分配给变量的对象的实际类型是什么,即使它可以从代码中确定。它只关心声明的类型。
    • @Eugene 的解释是正确的。按照他指向 JLS 的链接,或尝试将列表分配给 Integer 变量并查看错误消息的内容。 RHS 的类型为捕获类型,可赋值给 LHS 上的变量类型,但与变量类型不同。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多