Pages

Wednesday, November 24, 2010

compareTo must not use mutable fields

It is a rather common knowledge, that hashcode() and equals() methods must not use mutable fields. There are plenty of examples of breaking this contract on the net here and there. From the second link it is actually clear that the iterator's remove() method (iterator from the HashSet) is based on the hashcode as well as the HashSet's own remove() method. This not a very common knowledge.

And here is another fact with not very widespread awareness of. Implementing compareTo() method from the Comparable interface is as hard as implementing the hashcode method (and if you wish to use all the functionality of TreeSet inherited from the Collection interface it is as hard as implementing the equals() method too).

So here is the rather selfexplanatory code.

  public static void main(String[] args) {

    Set<MyClass> set = new TreeSet<MyClass>();
    MyClass entity = new MyClass("boo");

    for (int i = 0; i < 10; i++) {
      if (i == 5) {
        entity = new MyClass("boo" + i);
        set.add(entity);
      } else {
        set.add(new MyClass("boo" + i));
      }
    }
    System.out.println(set.contains(entity));

    entity.setContent("boo10");

    System.out.println(set.contains(entity));
  }

  private static class MyClass implements Comparable<MyClass> {
    private String content;

    public MyClass(String content) {
      this.content = content;
    }

    public void setContent(String content) {
      this.content = content;
    }

    @Override
    public int compareTo(MyClass o) {
      return content.compareTo((o).content);
    }
  }


* This source code was highlighted with Source Code Highlighter.

Output of this code is:
true
false


P.S. From the source of the TreeSet class it is clear that failures with mutated fields may be "less frequent" than with the HashSet, as the order of elements remain unchanged. I.e. you will be able to operate fully on the mutable object in the TreeSet all the time as long there is the single element in the TreeSet. It is not possible with HashSet. But of cause this is just a curious thing and no logic should be based on that. Here is the proof though (hashcode() and equals() are generated - do not bother yourself to read them ;-)):


  public static void main(String[] args) {
    Set<MyClass> set = new HashSet<MyClass>();
    MyClass entity = new MyClass("boo");
    set.add(entity);
 
    System.out.println(set.contains(entity));

    entity.setContent("boo10");

    System.out.println(set.contains(entity));
  }

  private static class MyClass {
    private String content;

    public MyClass(String content) {
      this.content = content;
    }

    public void setContent(String content) {
      this.content = content;
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result
          + ((content == null) ? 0 : content.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      MyClass other = (MyClass) obj;
      if (content == null) {
        if (other.content != null)
          return false;
      } else if (!content.equals(other.content))
        return false;
      return true;
    }
  }


* This source code was highlighted with Source Code Highlighter.



Output of this code:
true
false

No comments:

Post a Comment