獲取和放入原則

趙年峰發表於2012-12-08

原文連結: The Get and Put Principle

本文僅供學習和交流使用,如果您發現我已經侵犯到原作者的版權,請郵件我ttchgm@gmail.com。以便我及時刪除和處理。如果翻譯有錯誤或者交流可以隨時mail我。或者在sina微博 @天天吃好,私信與我。 本文拒絕任何形式轉載。

好的慣例才可能更好的嵌入萬用字元,但是如何決定在哪些情況下使用萬用字元呢?什麼地方使用extends,什麼地方使用super,什麼情況什麼地方不適合使用萬用字元?

幸運的是,一個簡單的原則規定決定了什麼情況下適合使用萬用字元、extends、super。

獲取和放入原則 : 僅當你從一個結構中獲取值的時候適合使用extends萬用字元,僅當你放入值到一個結構中的時候適合使用super萬用字元,當同時存在獲取或者放入操作的時候不能使用萬用字元。

我們已經看見這個原則存在copy函式的簽名中

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

函式在源列表中獲取值,所以宣告瞭一個extends萬用字元,放入到目的列表dst中,所以宣告瞭一個super萬用字元。每當你使用一個迭代器,你從一個結構中獲取值,所以使用一個extemds萬用字元。下面的函式從一個數字的集合中獲取值,都每個值都轉換成雙精度值,並把他們相加後的結果返回:

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 ,並且從 0 開始到整型 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);
}

一個集合同時呼叫sum和count,因此他的元素型別必須同時繼承Number(例如sum的需要),並且是Integer的超類(例如count的需要)。僅有Number和Integer這兩個類滿足同時呼叫sum和count的需求,我們摘取其中以個類呼叫,例子如下:

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

因為沒有萬用字元,引數必須是一個數字型的集合。如果你不喜歡在Number和 Integer之間選擇的話。 如果java可以允許你寫一個extends和super同時存在的萬用字元,你就不需要選擇了。如下例項,我們可以這樣寫:

double sumCount(Collection<? extends Number super Integer> coll, int n)
//java中不合法!

那麼我們在另一個數字型的集合裡或者一個整形的集合裡呼叫sumCount。但是java並不允許這樣。不合法的唯一原因是天真,真希望java未來的可以支援這樣。但是,現在,如果你需要同時獲取和放入那麼不能使用萬用字元。

Get和Put原則也工作在其他的方式。如果一個extends萬用字元出現,你能做的是獲取,而不是放入;並且如果一個super萬用字元出現,你能做的是放入,而不是獲取。

思考如下例子程式碼,使用一個extends萬用字元宣告一個列表:

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); // 編譯錯誤

sum呼叫沒問題,因為他從列表中獲取值,但add呼叫有問題,因為他把值放入一個列表。這樣不錯,不然的話我們就將一個雙精度值增加到了一個整型列表中!

相反的,思考如下程式碼段,使用一個extends萬用字元宣告一個列表:

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); // compile-time error

現在add呼叫沒問題,因為他放入一個值到列表中,但sum呼叫有問題,因為他從一個列表中獲取值。這樣不錯,因為一個列表的和不能包含一個毫無意義的字串!

異常能證明規則,並且不同規則都擁有一個異常。一個宣告為extends萬用字元的型別不能放入任何值,除了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]");

同樣,你不能從一個宣告為super萬用字元的型別裡獲得任何值,除了這個的型別是Object,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");

你是否可以發現,這有助於思考 ? extends T 包含null型別之上和T之下的界限內的任何型別(任何情況下型別null都是任何引用型別的子型別)。同樣的,你可以思考一下?super T包含T之下和Object之上的任何型別。

一個誘人的想法,extends萬用字元確保不可變,但是我們在早先的看到他並不是,指定一個列表型別為List<? Extends Number>,你仍然可以增加 null 值到列表中。你也可以移除列表中的元素(使用remove,removeAll或者retainAll)或者列表的排列(使用swap,sort,或者shuffle在便捷類Collections中,看17.1.1節)。

如果你確定想讓一個列表不會被改變,使用集合類中的函式unmodifiableList就可以做到這一點;類似的功能的奇特函式存在collection類中(譯者注:jdk1.6以上的版本存在於Collections便捷類中)(看17.3.2)節。如果確定想讓列表中的元素不能被改變,考慮使用規則來建立一個不可變的類,可以參看Joshua bloch的書 Effective Java(Addison-Wesley)中的第4章(item “Minimize mutability”/“Favor immutability”);

如下例子,在第二部分,在12.1節的CodingTask和PhoneTask類,在13.2節的類PriorityTask 是不可變的。

因為String 是final的,不能擁有子類,所以你可以認為List和List<? extends String >是同樣的型別。但事實上,前面的形式是後面的一個子類,並不是同一個型別,正如我們看見的應用程式的原則一樣。替換原則告訴我們他是一個子型別,因為他很容易通過一個值的型別形式,預測後面的型別。獲取和放入原則告訴我們,他不是同樣的型別,因為後面的型別定義不能add一個字串型別形式的值。

相關文章