Don't Use Mutable Object in HashSet


We know that we should not use mutable object as Key in HashMap, and should not use mutable fields in hashCode() if we plan it into HashSet or HashMap.
Check Consequences when using Mutable Fields in hashCode()

TL;DR
Be careful when use mutable objects in HashSet - bug hides there.
Instead, use HashMap.

But subtle bugs may hide if we use mutable objects in HashSet.

For example, we save set of configuration objects into db (as json string), later we read and update it - change field1 value from f1 to f2.

If we define hashCode and equals methods of configuration as below - only use name(the primary field).
@Test
public void testMutableHashSet() throws Exception {
    final Set<Confiuguration> sets = new HashSet<>();

    // we add name conf1 to hashset, and persit it as json string to db or file.
    Confiuguration conf1 = new Confiuguration();
    conf1.setName("name1");
    conf1.setField1("f1");
    sets.add(conf1);
    System.out.println(sets);

    // Later we want to change conf1 field1 to f2, we read the sets from db and deserialize it
    // into Hashset,
    // The following code will not work
    conf1 = new Confiuguration();
    conf1.setName("name1");
    conf1.setField1("f2");

    // not added, because name1-f1 and name1-f2 have same hashcode, and are equals
    final boolean added = sets.add(conf1);
    System.out.println(added);// false
    System.out.println(sets);// [Configuration [name=name1, field1=f1]]

    // we have to remove it first
    sets.remove(conf1);
    sets.add(conf1);
    System.out.println(sets);
}

class Confiuguration {
    private String name;
    private String field1;
    // other fields ignored
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

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

Hashset is actually a  hashmap - key is the hashset value, value is dummy object:
    // Dummy value to associate with an Object in the backing Map

    private static final Object PRESENT = new Object();
    private transient HashMap<E,Object> map;

Check put method in HashMap JDK7 Code, if hashmap already contains same key(same hashCode, and equals), hashmap will update the value. HashMap doesn't update existing key. - Thus hashset is not updated.
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

If we define hashCode and equals methods of configuration as below:
@Test
public void testMutableHashSet2() throws Exception {
    final Set<Confiuguration1> sets = new HashSet<>();

    // we add name conf1 to hashset, and persit it as json string to db or file.
    Confiuguration1 conf1 = new Confiuguration1();
    conf1.setName("name1");
    conf1.setField1("f1");
    sets.add(conf1);
    System.out.println(sets);

    // Later we want to change conf1 field1 to f2, we read the sets from db and deserialize it
    // into Hashset,
    // The following code will not work
    conf1 = new Confiuguration1();
    conf1.setName("name1");
    conf1.setField1("f2");

    // final boolean added = sets.add(conf1);
    // System.out.println(added);// true
    // System.out.println(sets);// [Confiuguration [name=name1, field1=f1], Confiuguration
    // // [name=name1, field1=f2]]

    // This has no effect, as it can't remove name1-f1, hashcode same, but not equals(field
    // values are not same)
    sets.remove(conf1);
    final boolean added = sets.add(conf1);
    System.out.println(added);

    // [Confiuguration [name=name1, field1=f1], Confiuguration [name=name1, field1=f2]]
    System.out.println(sets);
}
class Confiuguration1 {
    private String name;
    private String field1;
    // other fields ignored
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

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

Data structures:
If order doesn't matter - use Set not List, use Hashmap not TreeMap.

Labels

adsense (5) Algorithm (69) Algorithm Series (35) Android (7) ANT (6) bat (8) Big Data (7) Blogger (14) Bugs (6) Cache (5) Chrome (19) Code Example (29) Code Quality (7) Coding Skills (5) Database (7) Debug (16) Design (5) Dev Tips (63) Eclipse (32) Git (5) Google (33) Guava (7) How to (9) Http Client (8) IDE (7) Interview (88) J2EE (13) J2SE (49) Java (186) JavaScript (27) JSON (7) Learning code (9) Lesson Learned (6) Linux (26) Lucene-Solr (112) Mac (10) Maven (8) Network (9) Nutch2 (18) Performance (9) PowerShell (11) Problem Solving (11) Programmer Skills (6) regex (5) Scala (6) Security (9) Soft Skills (38) Spring (22) System Design (11) Testing (7) Text Mining (14) Tips (17) Tools (24) Troubleshooting (29) UIMA (9) Web Development (19) Windows (21) xml (5)