Сохранение измененных объектов, отсортированных в 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();
}
}
}