Java 中實現集合的 keep in order (後續)

weixin_33766168發表於2016-04-18

寫完上一篇「Java 中實現集合的 keep in order」後,自己又進行了一番探索,結合在公司專案的實際測試後,總結了一個更加有效地、基於 TreeSet(紅黑樹)的結構來實現集合的 keep in order,由於使用二叉樹來儲存有序集合,因此對集合的增加、刪除、查詢的時間複雜度均為 log(n)

集合(Set)的約定

Java 中對集合(Set)一般做法是:為了程式碼的健壯性,儘量在 Set 中儲存不可變的物件。因為不管是 HashSet 還是 TreeSet,他們都依賴元素自身的特性來保證集合的不重複性。如 HashSet 依賴元素的 equalshashCode 方法,TreeSet 依賴元素的 compareTo 方法或自定義的 Comparator

元素改變情況下保持集合有序

首先,公司專案裡,伺服器維護了一個玩家的集合:

class PlayerManager {
    
    // 玩家的 Map,<玩家 id -> 玩家實體>
    Map<Integer, Player> map = new ConcurrentHashMap<>();
}

// 玩家實體
interface Player {
    
    int getId();
    
    int getMoney();
    
    void setMoney(int money);
}

這個 map 將成為資料來源,它不一定是有序。現在增加一個成員:

// 可重排序的集合,儲存不可變物件:玩家 id
Set<Integer> reorderableSet;

這個集合記錄這所有玩家的 id 值,並以某種順序進行排序,排序的規則視需求而定,假如要以玩家擁有的金錢數來做財富排行榜。
由於玩家的財富值始終在發生變化,因此我定義一個介面來感知這種變化:

interface Reorderable<E> {

    // 這個方法用於重新計算元素 E 在集合中的索引
    void recomputeOrder(E element, ElementChangedCallback<E> callback);

    // 這是一個回撥,處理元素值改變的程式碼
    interface ElementChangedCallback<E> {

        void onElementChanged(E element);
    }
}

然後用 TreeSet 的子類實現上面的 Reorderable 介面來構造一個可重排序的集合:

class ReorderableSet<E> extends TreeSet<E> implements Reorderable<E> {

    // 使用自定義的比較器
    public ReorderableSet(Comparator<? super E> comparator) {
        super(comparator);
    }

    @Override
    public void recomputeOrder(E element, ElementChangedCallback<E> callback) {
        if (contains(element)) {
            // 先將元素從集合中移除,時間複雜度 log(n)
            remove(element);
            // 再使用回撥去改變元素的值
            callback.onElementChanged(element);
            // 在將元素新增到集合裡,時間複雜度 log(n)
            add(element);
        }
    }
}

因為是實現一個財富排行榜,所以定義排序規則為簡單的比較一下金錢數目即可:

// 這裡的 Integer 代表玩家 id
class MoneyComparator implements Comparator<Integer> {

    @Override
    public int compare(Integer A, Integer B) {
        
        // 從伺服器維護的玩家集合中獲取玩家的引用
        Player playerA = map.get(A);
        Player playerB = map.get(B);
        
        // 降序排列
        return playerB.getMoney() - playerA.getMoney();
    }
}

這時候可以構造之前定義的 Set<Player> reorderableSet 了:

Set<Player> reorderableSet = new ReorderableSet<>(new MoneyComparator());

要響應玩家金錢的變化,則構造一個實現 ElementChangedCallback 介面的類,並把它放在任何玩家金錢改變的地方:

class UpdateMoney implements Reorderable.ElementChangedCallback<Integer> {

    int money;

    UpdateMoney(int money) {
        this.money= money;
    }

    @Override
    public void onElementChanged(Integer playerId) {
        // 玩家金錢改變
        Player player = map.get(playerId);
        player.setMoney(money);
    }
}

玩家金錢改變的時候呼叫一下程式碼,比如買東西的時候

void buyGood(Player player, int cost) {
    Reorderable<Integer> reorderableSet = ......; // 獲得引用
    int moneyRemain = player.getMoney() - cost;
    
    // 構造 UpdateMoney 回撥
    reorderableSet .recomputeOrder(player.getId(), new UpdateMoney(moneyRemain);
}

因此,就可以實現集合(Set)在元素值改變時保持有序了,由於 TreeSet 基於紅黑樹實現,對它的查詢新增刪除均很快。

總結

通用的框架就像下面這樣:

class ReorderableSet<E> extends TreeSet<E> implements Reorderable<E> {

    public ReorderableSet() {
    }

    public ReorderableSet(Comparator<? super E> comparator) {
        super(comparator);
    }

    public ReorderableSet(Collection<? extends E> c) {
        super(c);
    }

    public ReorderableSet(SortedSet<E> s) {
        super(s);
    }

    @Override
    public void recomputeOrder(E element, ElementChangedCallback<E> callback) {
        if (contains(element)) {
            // 先將元素從集合中移除,時間複雜度 log(n)
            remove(element);
            // 再使用回撥去改變元素的值
            callback.onElementChanged(element);
            // 在將元素新增到集合裡,時間複雜度 log(n)
            add(element);
        }
    }
}

interface Reorderable<E> {

    void recomputeOrder(E element, ElementChangedCallback<E> callback);

    interface ElementChangedCallback<E> {

        void onElementChanged(E element);
    }
}

考慮到執行緒安全,可以在 recomputeOrder 中進行加鎖操作。

相關文章