Сохранение измененных объектов, отсортированных в TreeSets во все времена

Мне стало известно, что TreeSet не сохраняет измененные объекты в отсортированном порядке, если значения атрибутов объекта впоследствии будут изменены. Например,

public class Wrap { 
    static TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>(){
        @Override
        public int compare(Student o1, Student o2) {            
            return o1.age - o2.age;
        }       
    }); 
    public static void main(String []args){
        Student s = new Student(10);
        ts.add(s); 
        ts.add(new Student(50));
        ts.add(new Student(30));
        ts.add(new Student(15));
        System.out.println(ts);
        s.age = 24;      //Here I change the age of a student in the TreeSet
        System.out.println(ts);     
    }
}
class Student{
    int age;
    Student(int age){
        this.age = age;
    }   
    @Override
    public String toString() {
        return "Student [age=" + age + "]";
    }   
}

Выход:

[Student [age=10], Student [age=15], Student [age=30], Student [age=50]]
[Student [age=24], Student [age=15], Student [age=30], Student [age=50]]

После того, как я изменил возраст конкретного ученика, а затем распечатаю TreeSet, набор кажется уже не отсортированным. Почему это происходит? и как держать его отсортированным всегда?

Ответ 1

Почему это происходит?

Поскольку набор не может контролировать все его объекты для изменений... Как это было бы возможно?

Такая же проблема возникает и для HashSets. Вы не можете изменять значения, влияющие на хэш-код объектов, когда объект HashSet содержит объект.

и как держать его отсортированным всегда?

Обычно вы удаляете элемент из набора, изменяете его и затем снова вставляете. Другими словами, изменение

s.age = 24;      //Here I change the age of a student in the TreeSet

в

ts.remove(s);
s.age = 24;      //Here I change the age of a student in the TreeSet
ts.add(s);

Вы также можете использовать, например, список, и вызвать Collections.sort в списке каждый раз, когда вы изменили объект.

Ответ 2

Вы можете использовать шаблон наблюдателя. Пусть ваш TreeSet реализует Observer и позволяет вашему Student расширить Observable. Единственное изменение, которое вам нужно сделать, это скрыть поле age путем инкапсуляции, чтобы у вас было больше внутреннего контроля над изменением.

Вот пример запуска:

public class ObservableTreeSet<O extends Observable> extends TreeSet<O> implements Observer {

    public ObservableTreeSet(Comparator<O> comparator) {
        super(comparator);
    }

    @Override
    public boolean add(O element) {
        element.addObserver(this);
        return super.add(element);
    }

    @Override
    @SuppressWarnings("unchecked")
    public void update(Observable element, Object arg) {
        remove(element);
        add((O) element);
    }

}

а также

public class Student extends Observable {

    private int age;

    Student(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (this.age != age) {
            setChanged();
        }

        this.age = age;

        if (hasChanged()) {
            notifyObservers();
        }
    }

    @Override
    public String toString() {
        return "Student [age=" + age + "]";
    }
}

Теперь new TreeSet new ObservableTreeSet вместо new TreeSet.

static TreeSet<Student> ts = new ObservableTreeSet<Student>(new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.getAge() - o2.getAge();
    }
});

Это уродливо с первого взгляда, но вы не вносите изменений в основной код. Просто выполните s.setAge(24) и TreeSet будет "переупорядочивать" себя.

Ответ 3

Это общая проблема с картами и наборами. Значения вставляются с использованием hashCode/equals/compare в момент вставки, и если значения, на которых основаны эти методы, меняются, то структуры могут испортиться.

Один из способов - удалить элемент из набора и повторно добавить его после изменения значения. Тогда это будет правильно.

Ответ 4

Застекленные списки могут помочь: http://www.glazedlists.com/

Я использую его для своего EventList и не пробовал сортировку. Но на их домашней странице перечислены основные функции:

"Живая сортировка" означает, что ваш стол будет сортироваться по мере изменения ваших данных.

Ответ 5

Как правило, лучше всего вручную поддерживать упорядоченный Set/Map последовательно (см. Стратегию, упомянутую @aioobe).

Однако иногда это не вариант. В этих случаях мы можем попробовать следующее:

if (treeSet.contains(item)) {
    treeSet.remove(item);
    treeSet.add(item);
}

или с картой:

if (treeMap.containsKey(key)) {
    Value value = treeMap.get(key);
    treeMap.remove(key);
    treeMap.put(key, value);
}

Но это будет работать неправильно, потому что даже containsKey может привести к некорректному результату.

Итак, что мы можем сделать с грязной картой? Как мы можем обновить один ключ, не перестраивая всю карту? Вот утилита для решения этой проблемы (ее можно легко преобразовать в дескрипторы):

public class MapUtil {

    /**
     * Rearranges a mutable key in a (potentially sorted) map
     * 
     * @param map
     * @param key
     */
    public static <K, V> void refreshItem(Map<K, V> map, K key) {
        SearchResult<K, V> result = MapUtil.searchMutableKey(map, key);
        if (result.found) {
            result.iterator.remove();
            map.put(key, result.value);
        }
    }

    /**
     * Searches a mutable key in a (potentially sorted) map
     * 
     * Warning: currently this method uses equals() to check equality.
     * The returned object contains three fields:
     * - 'found': true iff the key found
     * - 'value': the value under the key or null if 'key' not found
     * - 'iterator': an iterator pointed to the key or null if 'key' not found
     * 
     * @param map
     * @param key
     * @return
     */
    public static <K, V> SearchResult<K, V> searchMutableKey(Map<K, V> map, K key) {
        Iterator<Map.Entry<K, V>> entryIterator = map.entrySet().iterator();
        while (entryIterator.hasNext()) {
            Map.Entry<K, V> entry = entryIterator.next();
            if (key.equals(entry.getKey())) {
                return new SearchResult<K, V>(true, entry.getValue(), entryIterator);
            }
        }
        return new SearchResult<K, V>(false, null, null);
    }

    public static class SearchResult<K, V> {

        final public boolean found;

        final public V value;

        final public Iterator<Map.Entry<K, V>> iterator;

        public SearchResult(boolean found, V value, Iterator<Map.Entry<K, V>> iterator) {
            this.found = found;
            this.value = value;
            this.iterator = iterator;
        }

    }

}

Ответ 6

Если ваша проблема связана с порядком итерации, и вы не хотите использовать дополнительные функции TreeSet (headSet() и т.д.), Используйте HashSet с пользовательским итератором. Кроме того, есть серьезная проблема с вашим примером: два ученика одного возраста (часто это бывает) конфликтуют.

Возможное решение:

public class Main {

    public static void main(final String[] args) {
        MagicSet<Student> ts = new MagicSet<Student>(new Comparator<Student>() {

            @Override
            public int compare(Student student1, Student student2) {
                return student1.age - student2.age;
            }

        });

        Student s = new Student(10);

        ts.add(s); 
        ts.add(new Student(50));
        ts.add(new Student(30));
        ts.add(new Student(15));

        System.out.println(ts); // 10, 15, 30, 50
        s.age = 24;
        System.out.println(ts); // 15, 24, 30, 50
    }

    public static class Student {

        public int age;

        public Student(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student [age=" + age + "]";
        }

    }

    public static class MagicSet<T> extends HashSet<T> {

        private static final long serialVersionUID = -2736789057225925894L;

        private final Comparator<T> comparator;

        public MagicSet(Comparator<T> comparator) {
            this.comparator = comparator;
        }

        @Override
        public Iterator<T> iterator() {
            List<T> sortedList = new ArrayList<T>();
            Iterator<T> superIterator = super.iterator();
            while (superIterator.hasNext()) {
                sortedList.add(superIterator.next());
            }
            Collections.sort(sortedList, comparator);
            return sortedList.iterator();
        }

    }

}