5

I am trying to understand why String and Stringbuilder/StringBuffer are treated differently when used as Hashmap keys. Let me make my confusion clearer with the following illustrations:

Example #1, using String:

String s1 = new String("abc");
String s2 = new String("abc");
HashMap hm = new HashMap();
hm.put(s1, 1);
hm.put(s2, 2);
System.out.println(hm.size());

Above code snippet prints '1'.

Example #2, using StringBuilder(or StringBuffer):

StringBuilder sb1 = new StringBuilder("abc");
StringBuilder sb2 = new StringBuilder("abc");
HashMap hm = new HashMap();
hm.put(sb1, 1);
hm.put(sb2, 2);
System.out.println(hm.size());

The above code snippet prints '2'.

Could anyone please explain why the difference in behaviour.

ambar
  • 2,053
  • 6
  • 27
  • 32
  • 7
    Note that `sb1.equals(sb2)` is false. –  May 21 '14 at 17:50
  • 2
    Because they're entirely different things. A string is a string. A string builder isn't a string until you convert it into one. – Dave Newton May 21 '14 at 17:51
  • Can you explain why you believe they should be the same? – Peter Lawrey May 21 '14 at 17:52
  • What is the exact difference that creates this inconsistency. Both are internally char[]. Aren't they? – ambar May 21 '14 at 17:54
  • String is immutable which means it won't change. StringBuilder is mutable which means that just because two StringBuilder happen to contain the same text now, doesn't mean they will in the future. – Peter Lawrey May 21 '14 at 18:08

4 Answers4

4

StringBuilder/Buffer do not override hashCode and equals. This means each instance of the object should be a unique hash code and the value or state of it does not matter. You should use the String for a key.

StringBuilder/Buffer is also mutable which is generally not a good idea to use as a key for a HashMap since storing the value under it can cause the value to be inaccessible after modification.

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/lang/StringBuilder.java

2

StringBuilder uses Object's default hashcode() implementation, whereas Strings are compared by value for the map keys.

The way that Map works (specifically HashMap) is that it utilizes an object's hashcode, not the contents of the class.

Note you lack parameterization on your maps:

HashMap yourMap = new HashMap();
//Should be
Map<String, Integer> yourMap = new HashMap<>();

And that there is no reason to create new string objects rather than assigning interned literals:

String s1 = "abc";
Rogue
  • 11,105
  • 5
  • 45
  • 71
0

I noticed you are using new String("abc"); which means you know that String a = "abc" and String b = "abc" are the same.

So, a == b returns true. and a.equals(b) returns true.

However the same thing doesn't work for StringBuffers, because its equals doesn't take into account the value of the object, only its hashcode.

If you look into StringBuffer you will see that it uses Object.equals, which is

public boolean equals(Object obj) {
    return (this == obj);
}

While the equals for String is:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                        return false;
                i++;
            }
            return true;
        }
    }
    return false;
}
Alexandre Santos
  • 8,170
  • 10
  • 42
  • 64
  • 1
    _So, a == b returns true..._ No it doesn't, those strings are different objects since they were created with `new` and not interned. – GriffeyDog May 21 '14 at 18:15
  • Try it for yourself: `String a = "abc"; String b = "abc"; System.out.println(a == b);` – Alexandre Santos May 21 '14 at 18:18
  • You should try it yourself. The code was `String s1 = new String("abc");`, not `String a = "abc";`. Note the use of `new`. – GriffeyDog May 21 '14 at 18:20
  • Oh man, read the code I posted. I didn't use the one provided by the OP. Read what I said in the beginning of my post. – Alexandre Santos May 21 '14 at 18:21
  • I see that now, although I'm not sure how that applies to the OPs question, which comes down to `StringBuilder` not overriding `equals` and `hashCode`. Anyway, sorry for the misread. – GriffeyDog May 21 '14 at 18:23
  • No worries, I have done the same in the past; Now, on the issue if the code applies to the OPs question, only him/her can decide it. You could ask him/her if you want. – Alexandre Santos May 21 '14 at 18:27
0

To understand the behaviour it is important to understand how Hashmaps work. Hashmaps use hash code value returned by the key objects (String objects in this case) to store them into appropriate memory compartments referred to as buckets. So, when retrieving the value associated with it hashmaps need to locate the compartment where the key is stored and then return the value against that key. Hashmaps identify the compartment from the hash code value of the key.

Now, imagine, if I have two objects which have same hash code value, what would happen? In such case the hashmap needs to know first whether both objects are same because if they are, then it would mean only one entry in the map and the existing value associated with that key will be replaced with the new value. But, merely having same hash code does not mean both keys are equal. So, the equality is determined by calling .equals() method on the key objects. If .equals() returns true then the objects are equal and in such a case the hashmaps need to update the value for the existing entry. But what if .equals() returns false? In that case both objects are different and should be stored as separate entries. So, they are stored side by side in the same compartment. So, when retrieving the value, the input key's hashcode is used to reach the compartment where it is stored and then if the compartment contains more than one objects then the input key is checked for equality with each object in the compartment and if matched then the associated value is returned.

Now, lets apply the above theory to the code you have. String objects are equal if their contents are equal. And by rule, if two objects are equal they should return same hash code. But remember, converse is not true. If two objects return same hash code that does not require them to be equal. This seems confusing at first but you can get over it in a few iterations. Two strings with same contents are equal and return same hash code even if they are physically different objects and hence when used as key in hashmap would always map to the same entry. And hence the behaviour.

The String class overrides the default equals() method which says two objects are equal if they have same references with the one which relies on the contents for equality. And it can do so because Strings are immutable. But, StringBuffer does not do that. It still relies on reference equality.

Drona
  • 6,886
  • 1
  • 29
  • 35