Skip to content

Files

Latest commit

 

History

History
163 lines (124 loc) · 7.62 KB

04_The_Get_and_Put_Principle.md

File metadata and controls

163 lines (124 loc) · 7.62 KB

《《《 返回首页
《《《 上一节

获取和放置原则

尽可能插入通配符可能是一个好习惯,但是如何决定使用哪个通配符? 你应该在哪里使用 extends,你应该在哪里使用 super,在哪里不适合使用通配符?

幸运的是,一个简单的原则决定哪一个是合适的。

获取和放置原则:当您只将值从结构中取出时,使用扩展通配符,当您只将值放入结构中时使用超级通配符,并且在双方都得到并放置时不使用通配符。

我们已经在复制方法的签名中看到了这个原理:

  public static <T> void copy(List<? super T> dest, List<? extends T> src)

该方法从源 src 中获取值,因此使用扩展通配符声明值,并将值放入目标 dst 中,因此使用超级通配符声明值。

无论何时使用迭代器,都会从结构中获取值,因此请使用扩展通配符。 这是一个需要一个数字集合的方法,每个转换为一个双精度求和:

  public static double sum(Collection<? extends Number> nums) {
    double s = 0.0;
    for (Number num : nums){
      s += num.doubleValue();
    }
    return s;
  }

由于这个使用 extends,所有以下的调用是合法的:

  List<Integer> ints = Arrays.asList(1,2,3);
  assert sum(ints) == 6.0;
  List<Double> doubles = Arrays.asList(2.78,3.14);
  assert sum(doubles) == 5.92;
  List<Number> nums = Arrays.<Number>asList(1,2,2.78,3.14);
  assert sum(nums) == 8.92;

如果不使用 extends,前两个调用将不合法

每当你使用add方法时,你把值放到一个结构中,所以使用 super 通配符。 这是一个采用数字和整数n的集合的方法将从零开始的前n个整数放入集合中:

  public static void count(Collection<? super Integer> ints, int n) {
    for (int i = 0; i < n; i++) ints.add(i);
  }

由于这使用 super,以下所有调用都是合法的:

  List<Integer> ints = new ArrayList<Integer>();
  count(ints, 5);
  assert ints.toString().equals("[0, 1, 2, 3, 4]");
  List<Number> nums = new ArrayList<Number>();
  count(nums, 5); nums.add(5.0);
  assert nums.toString().equals("[0, 1, 2, 3, 4, 5.0]");
  List<Object> objs = new ArrayList<Object>();
  count(objs, 5); objs.add("five");
  assert objs.toString().equals("[0, 1, 2, 3, 4, five]");

如果 super 不被使用,最后两个调用将是不合法的。无论何时您将值放入并从同一结构中获取值,都不应使用通配符。

  public static double sumCount(Collection<Number> nums, int n) {
    count(nums, n);
    return sum(nums);
  }

集合被传递给 sumcount,所以它的元素类型都必须继承 Number(按总数要求),Integer 的超类(按计数要求)。 唯一满足这两个约束的两个类是 NumberInteger,我们选择了第一个。 以下是一个调用示例:

  List<Number> nums = new ArrayList<Number>();
  double sum = sumCount(nums,5);
  assert sum == 10;

由于没有通配符,参数必须是 Number 的集合。

如果您不喜欢在 NumberInteger 之间进行选择,那么您可能会想到,如果 Java 允许您使用 extendssuper 编写通配符,则不需要选择。 例 如,我们可以写下以下内容:

  double sumCount(Collection<? extends Number super Integer> coll, int n)
  // 这在java里面是非法的

然后我们可以在一个数字集合或一个整数集合上调用 sumCount。 但 Java 不允许这样做。 打乱它的唯一原因是简单,可以想象 Java 在将来可能会支持这种表 示法。 但是,现在,如果你想同时获取和放置不要使用通配符。

获取和放置原则也是相反的。 如果扩展通配符存在,几乎所有的都是获取但不是放置类型的值;如果存在一个超级通配符,几乎所有你能够做的就是放置,但是不能获得 这种类型的值。

例如,考虑下面的代码片段,它使用一个用扩展通配符声明的列表:

  List<Integer> ints = new ArrayList<Integer>();
  ints.add(1);
  ints.add(2);
  List<? extends Number> nums = ints;
  double dbl = sum(nums); // ok
  nums.add(3.14); // compile-time error

调用它求和是好的,因为它从列表中获取值,但调用 add 的不是,因为它将一个值放入列表中。 这也是一样,因为否则我们可以添加双整数列表!相反,考虑下面的 代码片段,它使用一个超级通配符声明的列表:

  List<Object> objs = new ArrayList<Object>();
  objs.add(1);
  objs.add("two");
  List<? super Integer> ints = objs;
  ints.add(3); // ok
  double dbl = sum(ints); // 编译报错

现在调用 add 是正常的,因为它将一个值放入列表中,但是对 sum 的调用不是,因为它从列表中获取值。 这也是一样,因为包含一个字符串的列表的总和是没有 意义的!

例外证明了这个规则,而且每个规则都有一个例外。 你不能把任何东西放到用扩展通配符声明的类型中 -- 除了属于每个引用类型的值为 null

  List<Integer> ints = new ArrayList<Integer>();
  ints.add(1);
  ints.add(2);
  List<? extends Number> nums = ints;
  nums.add(null); // ok
  assert nums.toString().equals("[1, 2, null]");

同样,你也不能从使用超级通配符声明的类型中获取任何东西 - 除了 Object 类型的值,它是每个引用类型的超类型:

  List<Object> objs = Arrays.<Object>asList(1,"two");
  List<? super Integer> ints = objs;
  String str = "";
  for (Object obj : ints) str += obj.toString();
  assert str.equals("1two");

你可能会觉得有帮助的想法? 将 T 扩展为包含每个类型的一个区间,该区间由下面的 null 类型和上面的 T(其中 null 的类型是每个引用类型的子类型) 限定。 同样,你可能会想到? super T 包含每个类型在由 T 和由上面的 Object 限定的区间中。

认为扩展通配符确保不变性是诱人的,但事实并非如此。正如我们前面看到的,给定一个列表 List <? extends Number>,你仍然可以添加 null 值列表。 您也 可以删除列表元素(使用 removeremoveAllretainAll)或对列表进行置换(在便捷类集合中使用交换,排序或随机操作;参见第 17.1.1 节)。 如果 要确保列表不能更改,请使用类 Collections 中的 unmodifiableList 方法; 其他集合类也有类似的方法(见 17.3.2 节)。 如果你想确保列表元素不能被 改变,可以考虑按照 Joshua Bloch 在他的书第四章有效 JavaAddison-Wesley)(“最小化可变性”/“Favour不变性”)中给出的类不可变的规则。; 例如, 在第二部分中,第 12.1 节中的类 CodingTaskPhoneTask 是不可变的,就像第 13.2 节中的类 PriorityTask 一样。

因为 Stringfinal 的,并且可以没有子类型,所以您可能期望 List <String>List <? extends String> 是相同的类型。 但实际上前者是后者 的一个子类型,但不是同样的类型,正如应用我们的原则所能看到的那样。 替代原则告诉我们这是一个子类型,因为传递前者类型的值是可以的。 获取和放置原则告诉 我们它不是相同的类型,因为我们可以添加一个字符串到前一个类型的值而不是后一个。

《《《 下一节  
《《《 返回首页