Концепция здесь называется 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, что вы можете добавить его в список А (потому что B распространяется A):
List<A> as = new ArrayList<A>();
as.add(new B());
Теперь, скажем, у вас есть метод, который имеет дело со списками А, потребляя его экземпляры:
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>
(invariace).