1. 泛型的缺点

泛型(参数化类型)有个优点,即确定了就不会改变,例如List = new ArrayList();确认了泛型的类型后,只能存入Number类型,无法再装入其他类型。这可以有效的确保在编译期存入对象是安全的。但缺点就是只能存入这个类型,连Numer的子类Integer都无法存入

2. 有限制通配符

为了加入更多的通用性,Java提供了一种特殊的参数化类型,有限制通配符类型,如下代码,使用了泛型E,在生成实例的时候确定类型,addAll可以添加E及其子类,不会报错。

  1. import java.util.ArrayList;
  2. import java.util.List;
  3. /**
  4. * @author: qujundong
  5. * @date: 2020/11/29 下午3:25
  6. * @description:
  7. */
  8. public class BoundedWildcard<E> {
  9. private List<E> list = new ArrayList<>();
  10. public void pushAll(Iterable<? extends E> input){
  11. for(E e : input)
  12. list.add(e);
  13. }
  14. @Override
  15. public String toString() {
  16. return "BoundedWildcard{" +
  17. "list=" + list +
  18. '}';
  19. }
  20. public static void main(String[] args) {
  21. BoundedWildcard<Object> card = new BoundedWildcard<>();
  22. List<String> strs = new ArrayList<>();
  23. List<Integer> ints = new ArrayList<>();
  24. strs.add("abc");
  25. strs.add("bcd");
  26. ints.add(1);
  27. ints.add(2);
  28. card.pushAll(strs);
  29. card.pushAll(ints);
  30. System.out.println(card);
  31. }
  32. }

3. 上界通配符,下界通配符

所以为了获得最大限度的灵活度,要在表示生产者或者消费者的输入参数上使用通配符类型。,但是如果输入既是生产者又是消费者,那就不要通配符。
如果参数化类型表示一个生产者T,就用<? extends T>,如果表示一个消费者T,就用<? super T>。

  1. package item31;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. /**
  5. * @author: qujundong
  6. * @date: 2020/11/29 下午3:25
  7. * @description:
  8. */
  9. public class BoundedWildcard<E> {
  10. private List<E> list = new ArrayList<>();
  11. public void pushAll(Iterable<? extends E> input){
  12. for(E e : input)
  13. list.add(e);
  14. }
  15. public void consumer(List<? super E> target, E src){
  16. target.add(src);
  17. }
  18. public void product(List<? extends E> src){
  19. for(E e : src){
  20. list.add(e);
  21. }
  22. }
  23. @Override
  24. public String toString() {
  25. return "BoundedWildcard{" +
  26. "list=" + list +
  27. '}';
  28. }
  29. }

上述代码consumer和product分别表示消费者和生产者,可以记为PECS:prodcucer-extends,consumer-super。这部分的内容在前文泛型详解中也说过。

两种声明swap函数的方式比较

// Two possible declarations for the swap method

public static <E> void swap(List<E> list, int i, int j);

public static void swap(List<?> list, int i, int j);

这两个声明中的哪一个更可取,为什么? 在公共API中,第二个更好,因为它更简单。 你传入一个列表(任何列表),该方法交换索引的元素。 没有类型参数需要担心。 通常,如果类型参数在方法声明中只出现一次,请将其替换为通配符。 如果它是一个无限制的类型参数,请将其替换为无限制的通配符; 如果它是一个限定类型参数,则用限定通配符替换它。
第二个swap方法声明有一个问题。 这个简单的实现不会编译:

public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

因为通配符类型列表无法修改,因此对上述代码进行改进得到:

public static void swap(List<?> list, int i, int j) {

    swapHelper(list, i, j);

}

// Private helper method for wildcard capture

private static <E> void swapHelper(List<E> list, int i, int j) {

    list.set(i, list.set(j, list.get(i)));

}

swapHelper方法知道该列表是一个List <E>。 因此,它知道从这个列表中获得的任何值都是E类型,并且可以安全地将任何类型的E值放入列表中。 这个稍微复杂的swap的实现可以干净地编译。 它允许我们导出基于通配符的漂亮声明,同时利用内部更复杂的泛型方法。 swap方法的客户端不需要面对更复杂的swapHelper声明,但他们从中受益。 辅助方法具有我们认为对公共方法来说过于复杂的签名。

总结:在你的API中使用通配符类型,虽然棘手,但使得API更加灵活。 如果编写一个将被广泛使用的类库,正确使用通配符类型应该被认为是强制性的。 记住基本规则: producer-extends, consumer-super(PECS)。 还要记住,所有ComparableComparator都是消费者。