这里的概念叫做variance(协变,逆变)。
假设你有以下两个类:
class A {}
class B extends A {}
在这种情况下,您可以说B 的实例是A 的实例。也就是说,下面的代码是完全有效的:
A instance = new B();
现在,Java 中的泛型类默认情况下是不变的。这意味着 List<B> 不是 List<A>。也就是说,下面的代码不会编译:
List<A> as = new ArrayList<B>(); // error - Type mismatch!
但是,如果您有 B 的实例,请确保可以将其添加到 A 的列表中(因为 B 扩展了 A):
List<A> as = new ArrayList<A>();
as.add(new B());
现在,假设您有一个通过使用 A 的实例来处理 A 列表的方法:
void printAs(List<A> as) { ... }
很想拨打以下电话:
List<B> bs = new ArrayList<B>();
printAs(bs); // error!
但是,它不会编译!如果您想进行这样的调用,您必须确保参数List<B> 是方法预期类型的子类型。这是通过使用协方差来完成的:
void printAs2(List<? extends A> as) { ... }
List<B> bs = new ArrayList<B>();
printAs2(bs);
现在,此方法采用List<? extends A> 的实例,List<B> extends List<? extends A> 确实如此,因为B extends A。这就是协方差的概念。
介绍完之后,我们可以回到你提到的HashSet的构造函数:
public HashSet(Collection<? extends E> c) { ... }
这意味着以下代码将起作用:
HashSet<B> bs = new HashSet<B>();
HashSet<A> as = new HashSet<A>(bs);
之所以有效,是因为HashSet<B> is a HashSet<? extends A>。
如果构造函数被声明为HashSet(Collection<E> c),那么第二行将无法编译,因为即使HashSet<E> extends Collection<E>,HashSet<B> extends HashSet<A>(不变)也不是真的。