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
這樣的宣告。