Consumer super

June 21, 2022

接續〈Producer extends〉的內容,若 BA 的次型態,然而 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> 語義來說,只知道 NodeT 會是 Square 或其父型態,ShapeSquare 的父型態,因而可以通過編譯,類似地,對 <? super Circle> 語義來說,T 實際上會是 Circle 或其父型態,而 ShapeCircle 的父型態,因而可以通過編譯。

為何要支援逆變性呢?假設你想設計一個群組物件,可以指定群組有哪些物品,放入的物品會是相同形態(例如都是 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.Arrayssort 方法進行排序,現在 Groupsort 方法,還是能這麼呼叫:

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.Arrayssort 方法進行排序,查看 AP I文件,可以發現 sort 方法的第二個參數,也是宣告為 Comparator<? super T>,這也是為了讓客戶端,可以從父型態觀點實作 Comparator

作為消費者

來討論一個問題,對於底下的程式碼:

Node<? super Circle> node = new Node<>(new Circle(0, 0, 10), null);

node.value 的型態會是 ? super Circle,可接受 CircleShape 實例的指定,若要指定 node.value 給其他變數參考,該變數可以是什麼型態呢?

因為型態是 ? super Circlenode.value 可能參考的是 CircleShapeObject 型態的實例,因而,Circle circle = node.valueShape shape = node.value 是不行的,若 node.value 實際是 Object 就完了,編譯器只允許 Object o = node.value

也就是說,當類別支援泛型,而宣告時使用 ? superT 宣告的變數可作為消費者,也就是接收指定的角色,不適合作為提供資料的角色,記憶的口訣是「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) 會引發編譯錯誤。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter