Producer extends
June 21, 2022在定義泛型時,可以定義型態的邊界。例如:
class Animal {}
class Human extends Animal {}
class Toy {}
class Duck<T extends Animal> {}
public class BoundDemo {
public static void main(String[] args) {
Duck<Animal> ad = new Duck<Animal>();
Duck<Human> hd = new Duck<Human>();
Duck<Toy> td = new Duck<Toy>(); // 編譯錯誤
}
}
使用 extends
在上例中,使用 extends 限制 T 實際型態,必須是 Animal 的子類別,你可以使用 Animal 與 Human 指定 T 實際型態,但不可以使用 Toy,因為 Toy 不是 Animal 子類別。
實際應用的場景,可以用快速排序法的例子來說明:
package cc.openhome;
import java.util.Arrays;
public class Sort {
public static <T extends Comparable<T>> T[] sorted(T[] array) {
T[] arr = Arrays.copyOf(array, array.length);
sort(arr, 0, arr.length - 1);
return arr;
}
private static <T extends Comparable<T>> void sort(
T[] array, int left, int right) {
if(left < right) {
var q = partition(array, left, right);
sort(array, left, q - 1);
sort(array, q + 1, right);
}
}
private static <T extends Comparable<T>> int partition(
T[] array, int left, int right) {
var i = left - 1;
for(var j = left; j < right; j++) {
if(array[j].compareTo(array[right]) <= 0) {
i++;
swap(array, i, j);
}
}
swap(array, i+1, right);
return i + 1;
}
private static void swap(Object[] array, int i, int j) {
var t = array[i];
array[i] = array[j];
array[j] = t;
}
}
物件要能排序,方式之一是物件本身能比較大小,因此 Sort 類別實例要求 sorted 方法傳入的陣列,其中元素必須是 T 型態,<T extends Comparable<T>> 限制 T 必須實作 java.lang.Comparable<T> 介面。可以如下使用 sorted 方法:
package cc.openhome;
public class SortDemo {
public static void main(String[] args) {
String[] strs = {"3", "2", "5", "1"};
for(var s : Sort.sorted(strs)) {
System.out.println(s);
}
}
}
String 實作了 Comparable 介面,因此可如粗體字方式使用 sorted 方法排序,傳回新的已排序陣列。
若 extends 之後指定了類別或介面,想再指定其他介面,可以使用 & 連接。例如:
public class Some<T extends Iterable<T> & Comparable<T>> {
...
}
通配字元 ?
接著來看看型態通配字元 ?。若定義了以下類別:
package cc.openhome;
public class Node<T> {
public T value;
public Node<T> next;
public Node(T value, Node<T> next) {
this.value = value;
this.next = next;
}
}
如果有個 Shape 介面與實作如下:
interface Shape {
double area(); // 計算面積
}
record Circle(double x, double y, double radius) implements Shape {
@Override
public double area() {
return radius * radius * 3.14159;
}
}
record Square(double x, double y, double length) implements Shape {
@Override
public double area() {
return length * length;
}
}
這邊為了簡化範例, Circle、Square 單純地設計為資料載體,也就是定義為 record 類別,這邊的 Shape 要求必須實現計算面積的 area 方法。
若有以下程式片段,會發生編譯錯誤:
Node<Circle> circle = new Node<>(new Circle(0, 0, 10), null);
Node<Shape> shape = circle; // 編譯錯誤,incompatible types
在這個片段中,circle 型態宣告為 Node<Circle>,shape 型態宣告為 Node<Shape>,那麼 Node<Circle> 具有 Node<Shape> 的行為嗎?顯然地,編譯器認為不是,不允許通過編譯。
接下來的說明中,若 B 是 A 的子類別,或者 B 實現了 A 介面,統稱 B 是 A 的次型態(subtype)。
若 B 是 A 的次型態,而 Node<B> 也視為 Node<A> 的次型態,因為 Node 保持了次型態的關係,稱 Node 具有共變性(Covariance)。從以上編譯結果可看出,Java 的泛型不具有共變性,不過可使用型態通配字元 ? 與 extends 宣告變數,達到類似共變性。例如以下可以通過編譯:
Node<Circle> circle = new Node<>(new Circle(0, 0, 10), null);
Node<? extends Shape> shape = circle; // 類似共變性效果
在上面片段使用了 <? extends Shape> 語法,? 代表 shape 參考的 Node 物件,不知道 T 實際型態,加上 extends Shape 表示雖然不知道 T 型態,但一定會是 Shape 的次型態。由於 circle 宣告為 Node<Circle>,Circle 是 Shape 的次型態,可以通過編譯。
一個實際應用的例子是:
package cc.openhome;
public class CovarianceDemo {
public static void main(String[] args) {
var c1 = new Node<>(new Circle(0, 0, 10), null);
var c2 = new Node<>(new Circle(0, 0, 20), c1);
var c3 = new Node<>(new Circle(0, 0, 30), c2);
var s1 = new Node<>(new Square(0, 0, 15), null);
var s2 = new Node<>(new Square(0, 0, 30), s1);
show(c3);
show(s2);
}
public static void show(Node<? extends Shape> n) {
Node<? extends Shape> node = n;
do {
System.out.println(node.value);
node = node.next;
} while(node != null);
}
}
在上例中,適當地搭配了 var 來宣告,可以減輕泛型撰寫上的負擔。show 方法目的是顯示全部的形狀節點,若參數 n 宣告為 Node<Shape> 型態,就只能接受 Node<Shape> 實例。
範例中 show 方法使用型態通配字元 ? 與 extends 宣告參數,令 n 具備類似共變性,show 方法就可接受 Node<Circle> 實例,也可接受 Node<Square> 實例。執行結果如下:
Circle[x=0.0, y=0.0, radius=30.0]
Circle[x=0.0, y=0.0, radius=20.0]
Circle[x=0.0, y=0.0, radius=10.0]
Square[x=0.0, y=0.0, length=30.0]
Square[x=0.0, y=0.0, length=15.0]
若宣告 ? 不搭配 extends,則預設為 ? extends Object。例如:
Node<?> node; // 相當於 Node<? extends Object>
以上的 node 可接受 Node<Object>、Node<Shape>、Node<Circle> 等物件,只要角括號中的物件是 Object 的次型態,都可通過編譯。
這與宣告為 Node<Object> 不同,若 node 宣告為 Node<Object>,就只能參考至 Node<Object> 實例,也就是以下會編譯錯誤:
Node<Object> node = new Node<Integer>(1, null);
但以下會編譯成功:
Node<?> node = new Node<Integer>(1, null);
作為生產者
使用通配字元 ? 與 extends 限制 T 型態,T 宣告的變數取得之物件,只能指定給 Object 或 Shape,或將 T 宣告的變數指定為 null,除此之外不能進行其他動作。例如:
Node<? extends Shape> node = new Node<>(new Circle(0, 0, 10), null);
Object o = node.value;
node.value = null;
Circle circle = node.value; // 編譯錯誤
node.value = new Circle(0, 0, 10); // 編譯錯誤
以上程式片段,只知道 value 參考的物件會繼承 Shape,實際上會是 Circle 還是 Square 呢?若 node.value 是 Square 實例,指定給 Circle 型態的 circle 當然不對,因而編譯錯誤。如果建立 Node 時指定 T 型態是 Square,將 Circle 實例指定給 node.value 就不符合原先要求,因此也是編譯錯誤。
泛型的型態資訊僅提供編譯器進行型態檢查,編譯器不無法考慮執行時期物件實際型態(又稱型態抹除),因而造成以上的限制。
扣除可將 T 宣告的名稱指定為 null 的情況,簡單來說,對於支援泛型的類別,像是先前範例的 Node 類別定義,若使用 ? extends Shape 時,Node 類別定義中的 T 宣告之變數,只能作為資料的提供者,不能被重新設定,若 Node 類別使用 T 宣告某方法的參數,那麼呼叫該方法時會編輯錯誤,若使用T宣告某方法的傳回型態,呼叫該方法是沒有問題的。
實際應用案例之一是支援泛型的 java.util.List,若有個 List<? extends Shape> lt = new ArrayList<>(),那麼 lt.add(shape) 會引發編譯錯誤,然而 lt.get(0) 沒有問題。
既然泛型不考慮執行時期物件型態,因而造成這類限制,不如就活用這個限制, 記憶的口訣就是「Producer extends」,若想限定接收到的 List 實例,只作為資料的提供者,就使用 ? extends 這樣的宣告。


