Java 泛型何时需要 <? 扩展 T> 而不是 <T> 切换有什么缺点吗?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/897935/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 20:48:08  来源:igfitidea点击:

When do Java generics require <? extends T> instead of <T> and is there any downside of switching?

javagenericsjunit

提问by Yishai

Given the following example (using JUnit with Hamcrest matchers):

给出以下示例(使用带有 Hamcrest 匹配器的 JUnit):

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));  

This does not compile with the JUnit assertThatmethod signature of:

这不会使用以下 JUnitassertThat方法签名进行编译:

public static <T> void assertThat(T actual, Matcher<T> matcher)

The compiler error message is:

编译器错误信息是:

Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
    <? extends java.io.Serializable>>>)

However, if I change the assertThatmethod signature to:

但是,如果我将assertThat方法签名更改为:

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

Then the compilation works.

然后编译工作。

So three questions:

所以三个问题:

  1. Why exactly doesn't the current version compile? Although I vaguely understand the covariance issues here, I certainly couldn't explain it if I had to.
  2. Is there any downside in changing the assertThatmethod to Matcher<? extends T>? Are there other cases that would break if you did that?
  3. Is there any point to the genericizing of the assertThatmethod in JUnit? The Matcherclass doesn't seem to require it, since JUnit calls the matches method, which is not typed with any generic, and just looks like an attempt to force a type safety which doesn't do anything, as the Matcherwill just not in fact match, and the test will fail regardless. No unsafe operations involved (or so it seems).
  1. 为什么当前版本不能编译?虽然我对这里的协方差问题有模糊的理解,但如果必须的话,我当然无法解释。
  2. assertThat方法更改为 有什么缺点Matcher<? extends T>吗?如果你这样做,还有其他情况会打破吗?
  3. assertThat在 JUnit中对方法进行泛化有什么意义吗?该Matcher级似乎并不需要它,因为JUnit的调用matches方法,它不与任何通用的,只是看起来像键入企图迫使一个类型安全这并不做任何事情,因为Matcher只会事实上并不匹配,无论如何测试都会失败。不涉及不安全的操作(或者看起来如此)。

For reference, here is the JUnit implementation of assertThat:

作为参考,这里是 JUnit 实现assertThat

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new java.lang.AssertionError(description.toString());
    }
}

采纳答案by Scott Stanchfield

First - I have to direct you to http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html-- she does an amazing job.

首先 - 我必须引导您访问http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html- 她的工作非常出色。

The basic idea is that you use

基本思想是你使用

<T extends SomeClass>

when the actual parameter can be SomeClassor any subtype of it.

当实际参数可以是SomeClass或其任何子类型时。

In your example,

在你的例子中,

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

You're saying that expectedcan contain Class objects that represent any class that implements Serializable. Your result map says it can only hold Dateclass objects.

您是说expected可以包含代表任何实现 的类的 Class 对象Serializable。您的结果地图说它只能保存Date类对象。

When you pass in result, you're setting Tto exactly Mapof Stringto Dateclass objects, which doesn't match Mapof Stringto anything that's Serializable.

当您在结果传递,你设置T准确MapStringDate类对象,这不匹配MapString,以任何的Serializable

One thing to check -- are you sure you want Class<Date>and not Date? A map of Stringto Class<Date>doesn't sound terribly useful in general (all it can hold is Date.classas values rather than instances of Date)

要检查的一件事 - 你确定你想要Class<Date>而不是Date吗?Stringto的映射Class<Date>一般来说听起来不是很有用(它所能保存的只是Date.class值而不是 的实例Date

As for genericizing assertThat, the idea is that the method can ensure that a Matcherthat fits the result type is passed in.

至于泛化assertThat,想法是方法可以确保Matcher传入适合结果类型的a 。

回答by erickson

The reason your original code doesn't compile is that <? extends Serializable>does notmean, "any class that extends Serializable," but "some unknown but specific class that extends Serializable."

你原来的代码不能编译的原因是,<? extends Serializable>确实不是意味着,“扩展序列化任何类”,而是“延伸序列化的一些未知的,但具体的类。”

For example, given the code as written, it is perfectly valid to assign new TreeMap<String, Long.class>()>to expected. If the compiler allowed the code to compile, the assertThat()would presumably break because it would expect Dateobjects instead of the Longobjects it finds in the map.

例如,鉴于编写的代码,分配new TreeMap<String, Long.class>()>给是完全有效的expected。如果编译器允许代码编译,则assertThat()可能会中断,因为它期望Date对象而不是Long它在映射中找到的对象。

回答by GreenieMeanie

One way for me to understand wildcards is to think that the wildcard isn't specifying the type of the possible objects that given generic reference can "have", but the type of other generic references that it is is compatible with (this may sound confusing...) As such, the first answer is very misleading in it's wording.

我理解通配符的一种方法是认为通配符不是指定给定泛型引用可以“拥有”的可能对象的类型,而是它与之兼容的其他泛型引用的类型(这听起来可能令人困惑...) 因此,第一个答案在措辞上非常具有误导性。

In other words, List<? extends Serializable>means you can assign that reference to other Lists where the type is some unknown type which is or a subclass of Serializable. DO NOT think of it in terms of A SINGLE LIST being able to hold subclasses of Serializable (because that is incorrect semantics and leads to a misunderstanding of Generics).

换句话说,这List<? extends Serializable>意味着您可以将该引用分配给其他列表,其中类型是某种未知类型,它是 Serializable 或 Serializable 的子类。不要将其视为能够容纳 Serializable 的子类的单个列表(因为这是不正确的语义并导致对泛型的误解)。

回答by GreenieMeanie

It boils down to:

归结为:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

You can see the Class reference c1 could contain a Long instance (since the underlying object at a given time could have been List<Long>), but obviously cannot be cast to a Date since there is no guarantee that the "unknown" class was Date. It is not typsesafe, so the compiler disallows it.

您可以看到类引用 c1 可以包含一个 Long 实例(因为在给定时间的基础对象可能是List<Long>),但显然不能强制转换为 Date,因为不能保证“未知”类是 Date。它不是类型安全的,因此编译器不允许它。

However, if we introduce some other object, say List (in your example this object is Matcher), then the following becomes true:

但是,如果我们引入其他对象,例如 List(在您的示例中,此对象是 Matcher),则以下情况变为真:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

...However, if the type of the List becomes ? extends T instead of T....

...但是,如果 List 的类型变为 ? 扩展 T 而不是 T....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won't compile

I think by changing Matcher<T> to Matcher<? extends T>, you are basically introducing the scenario similar to assigning l1 = l2;

我认为通过更改Matcher<T> to Matcher<? extends T>,您基本上是在引入类似于分配 l1 = l2; 的场景;

It's still very confusing having nested wildcards, but hopefully that makes sense as to why it helps to understand generics by looking at how you can assign generic references to each other. It's also further confusing since the compiler is inferring the type of T when you make the function call (you are not explicitly telling it was T is).

嵌套通配符仍然非常令人困惑,但希望通过查看如何为彼此分配泛型引用来帮助理解泛型是有意义的。这也更加令人困惑,因为编译器在您进行函数调用时会推断 T 的类型(您没有明确告诉它是 T 是)。

回答by newacct

what if you use

如果你使用

Map<String, ? extends Class<? extends Serializable>> expected = null;

回答by Yishai

Thanks to everyone who answered the question, it really helped clarify things for me. In the end Scott Stanchfield's answer got the closest to how I ended up understanding it, but since I didn't understand him when he first wrote it, I am trying to restate the problem so that hopefully someone else will benefit.

感谢所有回答问题的人,这确实帮助我澄清了一些事情。最后 Scott Stanchfield 的回答最接近我最终理解它的方式,但是由于我在他第一次写它时不理解他,我试图重述这个问题,希望其他人能从中受益。

I'm going to restate the question in terms of List, since it has only one generic parameter and that will make it easier to understand.

我将根据 List 重述这个问题,因为它只有一个通用参数,这将使其更容易理解。

The purpose of the parametrized class (such as List<Date>or Map<K, V>as in the example) is to force a downcastand to have the compiler guarantee that this is safe (no runtime exceptions).

参数化类(例如示例中的List<Date>或 Map <K, V>)的目的是强制向下转换并让编译器保证这是安全的(没有运行时异常)。

Consider the case of List. The essence of my question is why a method that takes a type T and a List won't accept a List of something further down the chain of inheritance than T. Consider this contrived example:

考虑 List 的情况。我的问题的本质是为什么采用类型 T 和 List 的方法不会接受比 T 更远的继承链的 List 。考虑这个人为的例子:

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

This will not compile, because the list parameter is a list of dates, not a list of strings. Generics would not be very useful if this did compile.

这不会编译,因为 list 参数是日期列表,而不是字符串列表。如果编译成功,泛型就不会很有用。

The same thing applies to a Map<String, Class<? extends Serializable>>It is not the same thing as a Map<String, Class<java.util.Date>>. They are not covariant, so if I wanted to take a value from the map containing date classes and put it into the map containing serializable elements, that is fine, but a method signature that says:

同样的事情适用于 Map<String, Class<? extends Serializable>>它与 Map 不同<String, Class<java.util.Date>>。它们不是协变的,所以如果我想从包含日期类的映射中获取一个值并将其放入包含可序列化元素的映射中,那很好,但是方法签名表示:

private <T> void genericAdd(T value, List<T> list)

Wants to be able to do both:

希望能够同时做到:

T x = list.get(0);

and

list.add(value);

In this case, even though the junit method doesn't actually care about these things, the method signature requires the covariance, which it is not getting, therefore it does not compile.

在这种情况下,即使 junit 方法实际上并不关心这些事情,但方法签名需要协方差,它没有得到,因此它不会编译。

On the second question,

关于第二个问题,

Matcher<? extends T>

Would have the downside of really accepting anything when T is an Object, which is not the APIs intent. The intent is to statically ensure that the matcher matches the actual object, and there is no way to exclude Object from that calculation.

当 T 是一个对象时,会真正接受任何东西,这不是 API 的意图。目的是静态地确保匹配器与实际对象匹配,并且无法从该计算中排除 Object。

The answer to the third question is that nothing would be lost, in terms of unchecked functionality (there would be no unsafe typecasting within the JUnit API if this method was not genericized), but they are trying to accomplish something else - statically ensure that the two parameters are likely to match.

第三个问题的答案是,就未经检查的功能而言,不会丢失任何内容(如果此方法未被泛化,则 JUnit API 中不会有不安全的类型转换),但他们正在尝试完成其他事情 - 静态地确保两个参数很可能匹配。

EDIT (after further contemplation and experience):

编辑(经过进一步思考和经验):

One of the big issues with the assertThat method signature is attempts to equate a variable T with a generic parameter of T. That doesn't work, because they are not covariant. So for example you may have a T which is a List<String>but then pass a match that the compiler works out to Matcher<ArrayList<T>>. Now if it wasn't a type parameter, things would be fine, because List and ArrayList are covariant, but since Generics, as far as the compiler is concerned require ArrayList, it can't tolerate a List for reasons that I hope are clear from the above.

assertThat 方法签名的一个大问题是试图将变量 T 与 T 的泛型参数等同起来。这是行不通的,因为它们不是协变的。因此,例如,您可能有一个 T,它是 aList<String>但随后将编译器计算出的匹配传递给Matcher<ArrayList<T>>. 现在,如果它不是类型参数,一切都会好起来,因为 List 和 ArrayList 是协变的,但是由于泛型,就编译器而言需要 ArrayList,它不能容忍 List,原因我希望很清楚从上面。

回答by Lucas Ross

I know this is an old question but I want to share an example that I think explains bounded wildcards pretty well. java.util.Collectionsoffers this method:

我知道这是一个老问题,但我想分享一个我认为很好地解释有界通配符的例子。java.util.Collections提供这种方法:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

If we have a List of T, the List can, of course, contain instances of types that extend T. If the List contains Animals, the List can contain both Dogs and Cats (both Animals). Dogs have a property "woofVolume" and Cats have a property "meowVolume." While we might like to sort based upon these properties particular to subclasses of T, how can we expect this method to do that? A limitation of Comparator is that it can compare only two things of only one type (T). So, requiring simply a Comparator<T>would make this method usable. But, the creator of this method recognized that if something is a T, then it is also an instance of the superclasses of T. Therefore, he allows us to use a Comparator of Tor any superclass of T, i.e. ? super T.

如果我们有一个 List of T,那么 List 当然可以包含扩展 的类型的实例T。如果列表包含动物,则列表可以同时包含狗和猫(均为动物)。狗有一个属性“woofVolume”,猫有一个属性“meowVolume”。虽然我们可能希望基于这些特定于 的子类的属性进行排序T,但我们怎么能期望这个方法做到这一点呢?Comparator 的一个限制是它只能比较只有一种类型 ( T) 的两个事物。因此,只需要 aComparator<T>将使此方法可用。但是,此方法的创建者认识到,如果某物是T,那么它也是 的超类的实例T。因此,他允许我们使用T或 的任何超类的比较器T,即? super T