问题
It came to my notice that a TreeSet doesn't keep the mutable objects in sorted order if object attribute values are changed later on. For example,
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 + "]";
}
}
The output is :
[Student [age=10], Student [age=15], Student [age=30], Student [age=50]]
[Student [age=24], Student [age=15], Student [age=30], Student [age=50]]
After I change the age of a particular student, and then print the TreeSet, the Set seems no longer in sorted order. Why does this happen? and how to keep it sorted always?
回答1:
Why does this happen?
Because the set cannot monitor all its objects for changes... How would it be able to do that?!
Same problem arises for HashSets
. You can't change values affecting an objects hash-code when a HashSet
holds the object.
and how to keep it sorted always?
You typically remove the element from the set, modify it, and then reinsert it. In other words, change
s.age = 24; //Here I change the age of a student in the TreeSet
to
ts.remove(s);
s.age = 24; //Here I change the age of a student in the TreeSet
ts.add(s);
You can also use for example a list, and call Collections.sort
on the list each time you've modified an object.
回答2:
You could make use of the observer pattern. Let your TreeSet
implement Observer and let your Student
extend Observable. The only change you need to make is to hide the age
field by encapsulation so that you have more internal control over the change.
Here's a kickoff example:
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);
}
}
and
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 + "]";
}
}
Now do a new ObservableTreeSet
instead of 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();
}
});
It's ugly at first sight, but you end up with no changes in the main code. Just do a s.setAge(24)
and the TreeSet
will "reorder" itself.
回答3:
This is a generic problem with Maps and Sets. The values are inserted using the hashCode/equals/compare at the moment of insertion, and if the values on which these methods are based change, then the structures can screw up.
One way would be to remove the item from the set and re-add it after the value has been changed. Then it would be correct.
回答4:
Glazed Lists can help: http://www.glazedlists.com/
I use it for its EventList and haven't tried sorting. But on their home page they list the main features:
Live Sorting means your table stays sorted as your data changes.
回答5:
Generally, it is best to manually keep your sorted Set
/Map
continuously consistent (see the strategy mentioned by @aioobe).
However, sometimes this is not an option. In these cases we can try this:
if (treeSet.contains(item)) {
treeSet.remove(item);
treeSet.add(item);
}
or with a map:
if (treeMap.containsKey(key)) {
Value value = treeMap.get(key);
treeMap.remove(key);
treeMap.put(key, value);
}
But this will not work correctly, because even containsKey
can result with an incorrect result.
So what can we do with a dirty map? How can we refresh a single key without having to rebuild the entire map? Here is a utility class to solve this problem (can be easily converted to handle sets):
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:
If your problem is the iteration order, and you don't want to use the extra functionality of TreeSet
(headSet()
etc.), then use HashSet
with custom iterator. Also, there is a major problem with your example: two students of the same age (often it happens) make conflict.
A possible solution:
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();
}
}
}
来源:https://stackoverflow.com/questions/8030016/keeping-mutable-objects-sorted-in-treesets-at-all-times