Java中静态变量类型中的通配符

huangapple go评论61阅读模式
英文:

A Java wildcard in the type of a static variable

问题

此Java 泛型 教程 中提到:

通配符还有一个优点,它们可以用在方法签名之外,比如字段、局部变量和数组的类型。下面是一个示例。

回到我们的形状绘制问题,假设我们想要保留绘制请求的历史记录。我们可以在 Shape 类内部的静态变量中维护历史记录,并让 drawAll() 方法将其传入的参数存储在历史字段中。

static List<? extends Shape>                     \\ 1
    history = new ArrayList<? extends Shape>();  \\ 2

public void drawAll(List<? extends Shape> shapes) {    \\ 3
    history.addLast(shapes);                           \\ 4
    for (Shape s: shapes) {                            \\ 5
        s.draw(this);                                  \\ 6
    }
}

(我在代码片段中添加了行号。)

关于这段代码片段,有三个问题让我感到困惑。

  1. 在第2行中,通配符符号被用来实例化一个对象。这个对象的类型是什么?在教程中迄今为止呈现的所有示例中,通配符符号只被用作形式类型参数,而不是实际的类型参数。实际的类型参数一直都是具体的类,你可以在某个源文件中阅读其类定义。

  2. 在第4行中,通过其 add 方法修改了 history 字段。然而,同一教程的前一页 表示,通过使用上界通配符类型声明的变量(例如 List<? extends Shape> shapes)修改对象是非法的。

  3. 在第1行中使用通配符在声明静态变量似乎与同一教程的后续页面中的以下内容相矛盾:

    这就是为什么在静态方法或初始化程序中引用类型声明的类型参数,或在静态变量的声明或初始化程序中引用类型参数都是非法的。

英文:

This Java tutorial on generics says:

> Wildcards also have the advantage that they can be used outside of method signatures, as the types of fields, local variables and arrays. Here is an example.
>
> Returning to our shape drawing problem, suppose we want to keep a history of drawing requests. We can maintain the history in a static variable inside class Shape, and have drawAll() store its incoming argument into the history field.
>
> static List<List<? extends Shape>> \ 1
> history = new ArrayList<List<? extends Shape>>(); \ 2
>
> public void drawAll(List<? extends Shape> shapes) { \ 3
> history.addLast(shapes); \ 4
> for (Shape s: shapes) { \ 5
> s.draw(this); \ 6
> }
> }

(I added the line numbers in the code snippet.)

There are three issues that baffle me about the code snippet.

  1. In line 2 the wildcard symbol is used to instantiate an object. What is the object's type? In all the examples presented thus far in the tutorial, the wildcard symbol was only used as a formal type parameter, not as an actual type argument. Actual type arguments have always been concrete classes, whose class definition you can read in some source file.

  2. In line 4 the history field is modified via its add method. However a previous page of the same tutorial says that it is illegal to modify objects via variables declared with an upper-bounded wildcard type e.g. List&lt;? extends Shape&gt; shapes.

  3. The use of a wildcard in line 1 in the declaration of a static variable seems to be at odds with the following words from a later page of the same tutorial:

    > That is why it is illegal to refer to the type parameters of a type declaration in a static method or initializer, or in the declaration or initializer of a static variable.

答案1

得分: 1

  1. ArrayList. The generics here are not used to 'make a new ?' - that cannot be done. You're simply making a new arraylist here, is all. That arraylist is then parameterized - it is bound to contain solely things whose type signature matches List<? extends Shape>. Because that's what is inside the outer <>. Making a new arraylist does not instantly fill it with a bunch of data. An ArrayList is created empty, and therefore, there is no need to make anything of the type in the outer <>. Some code will perhaps add some lists to this arraylist later on. If it's a List<Shape>, that's fine. If it's a List<Rectangle>, that is also fine - both are compatible with the List<? extends Shape> signature.

In line 4 the history field is modified via its add method.

That's a confusing, bordering on incorrect, way to say this. The history field is not modified at all; to modify it, you'd have to write history = something. No, the history field is referencing some object (it's like a page in an address book, it's not like a house) - history.add will dereference this reference (walk over to the actual house by going to the address - we aren't modifying the address book at all), and then we change the house.

says that it is illegal to modify objects via variables

This is an oversimplification that does not hold in all cases. It does here, but, the trick is, this arraylist is not declared via ? extends. Just look at it: It's declared as List<List<X>>. The component type (the type of the objects this list is going to contain) has no extends at all. Sure, the X does, but that's one layer deeper, and this oversimplified rule refers only to the immediate component (the thing inside the outer <>).

It's oversimplified because it actually applies PECS. Essentially, given:

  • The type itself (ArrayList/List here) declares a type var E.
  • You use this type using a ? extends wildcard (and note that just ? is short for ? extends Object, so is also an extends wildcard for the purposes of this logic).
  • Then there is nothing you can ever pass that is a valid E (Other than literally null, but that's not very useful).

That's because the actual E, which we don't know here because you declared it with a ? extends bound, could be Shape, or Square, or Circle. We just don't know. There is no java expression that has the property that it can legally be passed as method parameter where the parameter type is something we don't know, all we know is - it is Square, or Shape, or Circle.

Contrast with a ? super Square bound: Here we don't know what the actual type is either (still using ? here, so we don't know), but we do know it is Square, or Shape, or Object, and it can't be anything else. new Square() is valid here. Imagine these three methods:

void foo(Object o) {}
void foo(Shape o) {}
void foo(Square o) {}

new Square() is fine for any and all of them. Therefore, it's fine for void foo(? super Square foo) {}. That is itself not legal java, but void foo(E foo) {} is, and if E is bound as ? super Square - now you know why you can call .add(x) on a List<? super TypeOfX> whereas you can't call .add() at all on a List<? extends Type>. (except with literally null which fits all types, but that's an academic curiosity, not a useful thing to do).

Point is, we aren't calling .add on a List whose generics type is ? extends. We are calling .add on a List whose generics type is List<X>, where, sure, X does contain extends but that's not the relevant place to look.

<> syntax is used both to declare variables as well as using them.

Given:

String x;

we aren't using x, we are declaring: "I shall now declare there is a variable, named x, which is constrained: It shall ever only point at nothing (null), or, objects that are either instances of String or some subtype thereof (which for string trivially is just 'instances of String', as String is final so no subtypes can exist)".

Whereas with System.out.println(x) we aren't declaring a new variable named x, we are using it.

The same applies to generics, but it's more confusing in the sense that declaration and use looks highly similar.

There are 2 ways to create a type var:

class Foo<T> {} // this DECLARES a new type var, named 'T'

public <T> void foo() // This DECLARES a new type var, named 'T'

All other places is usage of it. The statement:

type parameters of a type declaration

Is referring to the T in class Foo<T>{}. You can't use those in a static anything.

static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>();

There are no type variables used here AT ALL. A type variable is something like T. There aren't any in this statement. <? extends Shape> is not a 'type variable'. It's a type bound. Type variables are, as stated above, The T in class Foo<T>{} or the T in <T> returnType methodName() {}. By convention they are single capital letters.

英文:
  1. ArrayList. The generics here are not used to 'make a new ?' - that cannot be done. You're simply making a new arraylist here, is all. That arraylist is then parameterized - it is bound to contain solely things whose type signature matches List&lt;? extends Shape&gt;. Because that's what is inside the outer &lt;&gt;. Making a new arraylist does not instantly fill it with a bunch of data. An ArrayList is created empty, and therefore, there is no need to make anything of the type in the outer &lt;&gt;. Some code will perhaps add some lists to this arraylist later on. If it's a List&lt;Shape&gt;, that's fine. If it's a List&lt;Rectangle&gt;, that is also fine - both are compatible with the List&lt;? extends Shape&gt; signature.

> In line 4 the history field is modified via its add method.

That's a confusing, bordering on incorrect, way to say this. The history field is not modified at all; to modify it, you'd have to write history = something. No, the history field is referencing some object (it's like a page in an address book, it's not like a house) - history.add will dereference this reference (walk over to the actual house by going to the address - we aren't modifying the address book at all), and then we change the house.

> says that it is illegal to modify objects via variables

This is an oversimplification that does not hold in all cases. It does here, but, the trick is, this arraylist is not declared via ? extends. Just look at it: It's declared as List&lt;List&lt;X&gt;&gt;. The component type (the type of the objects this list is going to contain) has no extends at all. Sure, the X does, but that's one layer deeper, and this oversimplified rule refers only to the immediate component (the thing inside the outer &lt;&gt;).

It's oversimplified because it actually applies PECS. Essentially, given:

  • The type itself (ArrayList/List here) declares a type var E.
  • You use this type using a ? extends wildcard (and note that just ? is short for ? extends Object, so is also an extends wildcard for the purposes of this logic).
  • Then there is nothing you can ever pass that is a valid E (Other than literally null, but that's not very useful).

That's because the actual E, which we don't know here because you declared it with a ? extends bound, could be Shape, or Square, or Circle. We just don't know. There is no java expression that has the property that it can legally be passed as method parameter where the parameter type is something we don't know, all we know is - it is Square, or Shape, or Circle.

Contrast with a ? super Square bound: Here we don't know what the actual type is either (still using ? here, so we don't know), but we do know it is Square, or Shape, or Object, and it can't be anything else. new Square() is valid here. Imagine these three methods:

void foo(Object o) {}
void foo(Shape o) {}
void foo(Square o) {}

new Square() is fine for any and all of them. Therefore, it's fine for void foo(? super Square foo) {}. That is itself not legal java, but void foo(E foo) {} is, and if E is bound as ? super Square - now you know why you can call .add(x) on a List&lt;? super TypeOfX&gt; whereas you can't call .add() at all on a List&lt;? extends Type&gt;. (except with literally null which fits all types, but that's an academic curiosity, not a useful thing to do).

Point is, we aren't calling .add on a List whose generics type is ? extends. We are calling .add on a List whose generics type is List&lt;X&gt;, where, sure, X does contain extends but that's not the relevant place to look.

&lt;&gt; syntax is used both to declare variables as well as using them.

Given:

String x;

we aren't using x, we are declaring: "I shall now declare there is a variable, named x, which is constrained: It shall ever only point at nothing (null), or, objects that are either instances of String or some subtype thereof (which for string trivially is just 'instances of String', as String is final so no subtypes can exist)".

Whereas with System.out.println(x) we aren't declaring a new variable named x, we are using it.

The same applies to generics, but it's more confusing in the sense that declaration and use looks highly similar.

There are 2 ways to create a type var:

class Foo&lt;T&gt; {} // this DECLARES a new type var, named &#39;T&#39;

public &lt;T&gt; void foo() // This DECLARES a new type var, named &#39;T&#39;

All other places is usage of it. The statement:

> type parameters of a type declaration

Is referring to the T in class Foo&lt;T&gt; {}. You can't use those in a static anything.

> java
&gt; static List&lt;List&lt;? extends Shape&gt;&gt; \\ 1
&gt; history = new ArrayList&lt;List&lt;? extends Shape&gt;&gt;();
&gt;

There are no type variables used here AT ALL. A type variable is something like T. There aren't any in this statement. &lt;? extends Shape&gt; is not a 'type variable'. It's a type bound. Type variables are, as stated above, The T in class Foo&lt;T&gt;{} or the T in &lt;T&gt; returnType methodName() {}. By convention they are single capital letters.

答案2

得分: 0

你提供的代码片段中,在第2行中将通配符用作List对象的类型参数。创建的对象类型是一个未知子类型的Shape的List。创建的List对象可以存储Shape的任何子类型,但在编译时未知确切的类型。

关于第4行中修改history字段的问题,教程可能指的是不能修改使用上界通配符类型声明的变量所引用的List对象的内容是不合法的。然而,在第4行中,shapes变量并没有被修改;相反,一个对它的引用被添加到history列表中。将一个类型为List<? extends Shape>的对象的引用添加到List<List<? extends Shape>>对象是合法的。

关于第1行中在静态变量的声明中使用通配符的问题,禁止在静态方法或初始化程序、静态变量的声明或初始化程序中引用类型声明的类型参数,这适用于封闭类的类型参数的使用。在你提供的示例中,没有使用封闭类Shape的类型参数;相反,将通配符用作List对象的类型参数。因此,与教程中提到的限制没有冲突。

英文:

The code snippet you've provided in your question is using a wildcard as a type argument for the List object in line 2. The type of the object created is List of an unknown subtype of Shape. The List object created can store any subtype of Shape, but the exact type is unknown at compile time.

Regarding the issue of modifying the history field in line 4, the tutorial might be referring to the fact that it is not legal to modify the contents of the List object referred to by a variable declared with an upper-bounded wildcard type. However, in line 4, the shapes variable is not being MODIFIED; instead, a reference to it is being ADDED to the history list. It is legal to add a reference to an object of type List<? extends Shape> to a List<List<? extends Shape>> object.

Regarding the use of a wildcard in the declaration of a static variable in line 1, the restriction on referring to type parameters of a type declaration in a static method or initializer, or in the declaration or initializer of a static variable, applies to the use of the type parameter of the enclosing class. In the example you provided, the type parameter of the enclosing class Shape is not being used; instead, a wildcard is being used as the type argument for the List object. Therefore, there is no conflict with the restriction mentioned in the tutorial.

huangapple
  • 本文由 发表于 2023年2月19日 18:00:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/75499300.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定