Consumer super
June 21, 2022接續〈Producer extends〉的內容,若 B
是 A
的次型態,然而 Node<A>
卻被視 Node<B>
的次型態,因為 Node
逆轉了次型態的關係,稱 Node
具有逆變性(Contravariance)。也就是說,若以下程式碼片段沒有發生錯誤,Node
具有逆變性:
Node<Shape> shape = new Node<>(new Circle(0, 0, 10), null);
Node<Square> node = shape; // 實際上會編譯錯誤
? 與 super
Java 泛型不支援逆變性,實際上第二行會發生編譯錯誤。可使用型態通配字元 ?
與 super
來宣告,達到類似逆變性的效果,例如:
Node<Shape> shape = new Node<>(new Circle(0, 0, 10), null);
Node<? super Square> node1 = shape;
Node<? super Circle> node2 = shape;
就 <? super Square>
語義來說,只知道 Node
的 T
會是 Square
或其父型態,Shape
是 Square
的父型態,因而可以通過編譯,類似地,對 <? super Circle>
語義來說,T
實際上會是 Circle
或其父型態,而 Shape
是 Circle
的父型態,因而可以通過編譯。
為何要支援逆變性呢?假設你想設計一個群組物件,可以指定群組有哪些物品,放入的物品會是相同形態(例如都是 Circle
),並有個 sort
方法,可指定 java.util.Comparator
,針對群組中的物品排序,請問以下泛型該如何宣告?
public class Group<T> {
public T[] things;
public Group(T... things) {
this.things = things;
}
public void sort(Comparator<__________> comparator) {
// 做一些排序
}
}
宣告為 Comparator<T>
的話,若是 Group<Circle>
實例,代表 sort
要傳入 Comparator<Circle>
,或許是想根據半徑排序;若是 Group<Square>
實例,代表 sort
要傳入 Comparator<Square>
,或許是要根據正方形邊長排序。
如果要能從父型態的觀點排序呢?例如,不管是 Group<Circle>
或 Group<Square>
,都要能接受 Comparator<Shape>
,根據 area
傳回值排序呢?
只是宣告為 Comparator<T>
的話,若是 Group<Circle>
實例,代表 sort
的參數型態是 Comparator<Circle>
,然而要能接受 Comparator,方才談到,Java不支援逆變性,然而可以使用
?與
super` 來宣告,達到類似逆變性的效果:
package cc.openhome;
import java.util.Arrays;
import java.util.Comparator;
public class Group<T> {
public T[] things;
public Group(T... things) {
this.things = things;
}
public void sort(Comparator<? super T> comparator) {
Arrays.sort(things, comparator);
}
}
為了簡化範例,sort
方法使用了 java.util.Arrays
的 sort
方法進行排序,現在 Group
的 sort
方法,還是能這麼呼叫:
var circles = new Group<Circle>(
new Circle(0, 0, 10), new Circle(0, 0, 20));
circles.sort((c1, c2) -> { // 指定 Comparator<Circle>,根據半徑排序,使用 Lambda 表示式實作
var diff = c1.radius() - c2.radius();
if(diff == 0.0) {
return 0;
}
return diff > 0 ? 1 : -1;
});
var squares = new Group<Square>(
new Square(0, 0, 20), new Square(0, 0, 30));
squares.sort((s1, s2) -> { // 指定 Comparator<Square>,根據邊長排序
var diff = s1.length() - s2.length();
if(diff == 0.0) {
return 0;
}
return diff > 0 ? 1 : -1;
});
若想從 T
的父型態的觀點實現 Comparator
,現在也沒問題了:
package cc.openhome;
import static java.lang.System.out;
import java.util.Arrays;
import java.util.Comparator;
public class ContravarianceDemo {
public static void main(String[] args) {
// 根據面積排序
Comparator<Shape> areaComparator = (s1, s2) -> {
var diff = s1.area() - s2.area();
if(diff == 0.0) {
return 0;
}
return diff > 0 ? 1 : -1;
};
var circles = new Group<Circle>(
new Circle(0, 0, 10), new Circle(0, 0, 20));
circles.sort(areaComparator);
Arrays.stream(circles.things).forEach(out::print);
out.println();
var squares = new Group<Square>(
new Square(0, 0, 20), new Square(0, 0, 30));
squares.sort(areaComparator);
Arrays.stream(squares.things).forEach(out::print);
out.println();
}
}
執行結果如下:
Circle[x=0.0, y=0.0, radius=10.0]Circle[x=0.0, y=0.0, radius=20.0]
Square[x=0.0, y=0.0, length=20.0]Square[x=0.0, y=0.0, length=30.0]
方才範例使用了 java.util.Arrays
的 sort
方法進行排序,查看 AP I文件,可以發現 sort
方法的第二個參數,也是宣告為 Comparator<? super T>
,這也是為了讓客戶端,可以從父型態觀點實作 Comparator
。
作為消費者
來討論一個問題,對於底下的程式碼:
Node<? super Circle> node = new Node<>(new Circle(0, 0, 10), null);
node.value
的型態會是 ? super Circle
,可接受 Circle
或 Shape
實例的指定,若要指定 node.value
給其他變數參考,該變數可以是什麼型態呢?
因為型態是 ? super Circle
,node.value
可能參考的是 Circle
、Shape
或 Object
型態的實例,因而,Circle circle = node.value
或 Shape shape = node.value
是不行的,若 node.value
實際是 Object
就完了,編譯器只允許 Object o = node.value
。
也就是說,當類別支援泛型,而宣告時使用 ? super
,T
宣告的變數可作為消費者,也就是接收指定的角色,不適合作為提供資料的角色,記憶的口訣是「Consumer super」。
同樣舉 java.util.List
為例,若有個 List<? super Circle> lt = new ArrayList<>()
,那麼 lt.add(shape)
不會有問題,lt
像是 Shape
的消費者,然而,Shape shape = lt.get(0)
或 Circle circle = lt.get(0)
會引發編譯錯誤。