Java 中实现集合的 keep in order

原问题

Java 中怎样实现一种即使元素改变依然有序的集合?

问题由来

起因是在公司做游戏项目的时候遇到一个需求需要实现:

服务器要维护一个帮派成员(Member)的集合,这个集合要按照在线状态成员等级名称依次有序排列

由于每时每刻都有玩家在不断上下线,成员的经验值也在不断的改变。因此这个集合的有序状态在不断变化,起初的想法是维护一个有序的列表 List<Member>Collections.sort() 加上一个定时结构,比如每十分钟对列表排序一次。然后客户端请求集合的时候返回这个列表即可。

这种方法的优点是排序是显而易见的:定时执行、开销相对小。类似的代码如下:

class Member {
    // 帮派成员类
}

// 维护有序列表
List<Member> orderedMembers = Collections.synchronizedList(new ArrayList<Member>());

// 在服务器的刷帧循环中定时排序
@Override
public void onFrameUpdated(long currentTimeMillis) {
    // 十分钟排序一次
    if (lastSorting + 10 * 60 * 1000 < currentTimeMillis) {
        // 排序 ....
        sort(orderedMembers);
    }
}

实时响应的排行榜

我想让这个集合能实时响应玩家自身的变化,而不是定时扫描一次,于是一开始我是这么尝试的,继承 TreeSet 然后实现一个重新排序的回调 ReorderCallback,在任何玩家经验值改变或是上线下线的时候调用回调的方法 reorder() 来使集合保持有序,代码如下

interface ReorderCallback<T> {

    // 回调方法,在任何影响排序的状态值改变的时候被调用
    void reorder(T element);
}

// 自定义了一个 Set 用于监测成员类的变化
class AlwaysOrderedSet<T> extends TreeSet<T> implements ReorderCallback<T> {

    // 实现回调
    @Override
    public void reorder(T element) {
        remove(element);
        add(element);
    }
}

然后在需要的地方回调:

// 帮派成员类
class Member {

    ReorderCallback rCallback;
    
    // 玩家上线
    @Override
    public void onOnline() {
        if (rCallback != null) {
            rCallback.reorder(this);
        }
    }
    
    // 玩家下线
    @Override
    public void onOffline() {
        if (rCallback != null) {
            rCallback.reorder(this);
        }
    }
}

然而,每次玩家状态改变后调用 reorder() 并不能保持原集合的有序,反而会重复添加 member 对象。因为 TreeSet 无法追踪元素的变化,就像以下的演示一样:

public class Sorter {

    public static void main(String[] args) {

        class Student implements Comparable<Student> {

            int id;
            String name;
            int age;

            Student(int id, String name, int age) {
                this.id = id;
                this.name = name;
                this.age = age;
            }

            @Override
            public String toString() {
                return String.format("id=%d, name=%s, age=%d", id, name, age);
            }

            @Override
            public int compareTo(Student o) {
                return o.age - this.age;
            }
        }

        Set<Student> alwaysOrdered = new TreeSet<>();

        Student a = new Student(1, "Amy", 50);
        Student b = new Student(2, "Bob", 30);
        Student c = new Student(3, "Chris", 40);

        alwaysOrdered.add(a);
        alwaysOrdered.add(b);
        alwaysOrdered.add(c);

        System.out.println("-- before --");
        alwaysOrdered.forEach(System.out::println);

        // 集合元素发生改变
        b.age = 100;

        System.out.println("-- after --");
        alwaysOrdered.forEach(System.out::println);

        // 这里就相当于回调,简单的 remove 再 add
        alwaysOrdered.remove(b);
        alwaysOrdered.add(b);

        System.out.println("-- after remove and add --");
        alwaysOrdered.forEach(System.out::println);
    }
}

上面的代码运行结果如下:

-- before --
id=1, name=Amy, age=50
id=3, name=Chris, age=40
id=2, name=Bob, age=30
-- after --
id=1, name=Amy, age=50
id=3, name=Chris, age=40
id=2, name=Bob, age=100
-- after remove and add --
id=2, name=Bob, age=100
id=1, name=Amy, age=50
id=3, name=Chris, age=40
id=2, name=Bob, age=100

可以看出,我试图改变 Bob 的年龄,然后先后调用集合的 removeadd 却并不是我预想的那样,remove 实际是返回了 false,因此 b 根本没有从集合中移除。

百思不得其解后我试图阅读 Set 的源代码,发现了一些线索:

 * <p>Note: Great care must be exercised if mutable objects are used as set
 * elements.  The behavior of a set is not specified if the value of an object
 * is changed in a manner that affects <tt>equals</tt> comparisons while the
 * object is an element in the set.  A special case of this prohibition is
 * that it is not permissible for a set to contain itself as an element.

意思是:

注:如果将可变对象用作 set 元素,那么必须极其小心。如果对象是 set 中某个元素,以一种影响 equals 比较的方式改变对象的值,那么 set 的行为就是不确定的。此项禁止的一个特殊情况是不允许某个 set 包含其自身作为元素。

TreeSet 的源码文档里是这么写的

 * <p>Note that the ordering maintained by a set (whether or not an explicit
 * comparator is provided) must be <i>consistent with equals</i> if it is to
 * correctly implement the {@code Set} interface.  (See {@code Comparable}
 * or {@code Comparator} for a precise definition of <i>consistent with
 * equals</i>.)  This is so because the {@code Set} interface is defined in
 * terms of the {@code equals} operation, but a {@code TreeSet} instance
 * performs all element comparisons using its {@code compareTo} (or
 * {@code compare}) method, so two elements that are deemed equal by this method
 * are, from the standpoint of the set, equal.  The behavior of a set
 * <i>is</i> well-defined even if its ordering is inconsistent with equals; it
 * just fails to obey the general contract of the {@code Set} interface.

大致意思是:

注意,如果要正确实现 Set 接口,则 set 维护的顺序(无论是否提供了显式比较器)必须与 equals 一致。(关于与 equals 一致 的精确定义,请参阅 ComparableComparator。)这是因为 Set 接口是按照 equals 操作定义的,但 TreeSet 实例使用它的 compareTo(或 compare)方法对所有元素进行比较,因此从 set 的观点来看,此方法认为相等的两个元素就是相等的。即使 set 的顺序与 equals 不一致,其行为也是定义良好的;它只是违背了 Set 接口的常规协定。

总的来说就是,Set 的定义是不包含重复元素的集合,这个不重复的特性由它维护的元素的 equals() 方法来保证,这个好理解,关键是下面。TreeSet 并不完全遵守这个定义,它的不重复性由元素的 compareTo() 方法来保证。

因此,即便元素重写了 equals() 方法和 hashCode() 方法,当元素 E 的值发生改变时,对于 TreeSet,对 contains(E) 的调用始终返回 false,因此对 remove(E) 的调用也始终返回 false,因为 TreeSet 只依据 compareTo 方法来判断两个元素是否相等并进行排序。

所以对于之前的 Student 示例,解决方法就是让 Student 实现 Comparable 并重写 compareTo 方法,用该方法代替 equals() 方法去定义对象是否相等的逻辑,同时加上排序:

class Student implements Comparable<Student> {

    int id;
    String name;
    int age;

    Student(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return String.format("id=%d, name=%s, age=%d", id, name, age);
    }

    // compareTo 方法有两个作用:1、代替 equals,2、compareTo
    @Override
    public int compareTo(Student o) {
        // 这里的判断非常关键,代替了 equals() 方法
        if (id == o.id) {
        return 0;
    }
                
    // 这里再进行原始的 compareTo 判断
    if (age == o.age) {
        if (name.equals(o.name)) {
            return id - o.id;
        }
        return name.compareTo(o.name);
    }
    return o.age - age;
    }
}

改写后的 Student 的代码运行结果如下:

id=1, name=Amy, age=50
id=3, name=Chris, age=40
id=2, name=Bob, age=30
-- after --
id=1, name=Amy, age=50
id=3, name=Chris, age=100
id=2, name=Bob, age=30
-- after remove and add --
id=3, name=Chris, age=100
id=1, name=Amy, age=50
id=2, name=Bob, age=30
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值