今天遇到一个bug,简单的说就是把自定义对象作为key 存到HashMap中之后,经过一系列操作(没有remove操作)之后 用该对象到map中取,返回null。
然后查看了HashMap的源代码,get方法的核心代码如下:
1 final Entry<K,V> getEntry(Object key) { 2 int hash = (key == null) ? 0 : hash(key); 3 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 4 e != null; 5 e = e.next) { 6 Object k; 7 if (e.hash == hash && 8 ((k = e.key) == key || (key != null && key.equals(k)))) 9 return e; 10 } 11 return null; 12 }
可以看出 hashmap在比较Key的时候,是先比较该key的hashcode. 返回entry条件是hashcode相同并且对象的地址或者equals方法比较相同。然后检查了出现bug的代码,因为重写了hashcode方法,然后修改了对象的属性(与hashcode计算相关)导致不太容易注意到的bug
验证HashMap的存储与hashCode,equals相关:
public class MapTest { public static void main(String[] args){ People people = new People(); people.age = 1; people.name = "test"; Map<People,Integer> map = new HashMap<People, Integer>(); map.put(people, 1); people.age = 2; System.out.println("同一对象修改hashcode:" + map.get(people)); People otherPeople = new People(); otherPeople.age = 1; otherPeople.name = "test"; System.out.println("不同对象有相同的hashcode与equals:" + map.get(otherPeople)); } static class People{ public int age; public String name; @Override public int hashCode() { return age; } @Override public boolean equals(Object obj) { if(obj == null || !(obj instanceof People)){ return false; } if(((People)obj).name != name){ return false; } return true; } } }
结果:
同一对象修改hashcode:null
不同对象有相同的hashcode与equals:1
然后考虑到HashSet是不是也有这样的特性: 因为set是一个不包含相同元素的collections,所以在判断set中是否含有同一个元素的时候,是不是也是根据hashcode跟equals来判断的,Set源码的add方法如下:
1 private transient HashMap<E,Object> map; 2 3 public HashSet() { 4 map = new HashMap<>(); 5 } 6 7 public boolean add(E e) { 8 return map.put(e, PRESENT)==null; 9 }
可以看出HashSet的add实际上使用HashMap来实现的,所以用的是同一个机制:判断是否包含某个对象,1.首先hashcode相同,2.然后地址或者equals方法比较相同,条件1跟2是&&关系,而不是||关系
这是hashmap/hashset的判断是否包含某个key或者元素的机制,然后看看其他map接口的实现类,比如ConcurrentSkipListMap:
concurrentSkipListMap#put方法核心代码:
1 Node<K,V> b = findPredecessor(key); 2 Node<K,V> n = b.next; 3 for (;;) { 4 if (n == null) 5 return null; 6 Node<K,V> f = n.next; 7 if (n != b.next) // inconsistent read 8 break; 9 Object v = n.value; 10 if (v == null) { // n is deleted 11 n.helpDelete(b, f); 12 break; 13 } 14 if (v == n || b.value == null) // b is deleted 15 break; 16 int c = key.compareTo(n.key); 17 if (c == 0) 18 return n; 19 if (c < 0) 20 return null; 21 b = n; 22 n = f; 23 } 24 }
以上代码第16行可看出:比较的时候是根据对象重写的compareTo方法来比较的,我们做个测试:
1 public class MapTest { 2 public static void main(String[] args){ 3 People people = new People(); 4 people.age = 1; 5 people.name = "test"; 6 Map<People,Integer> map = new ConcurrentSkipListMap<People, Integer>(); 7 map.put(people, 1); 8 people.age = 2; 9 System.out.println(map.get(people)); 10 People otherPeople = new People(); 11 otherPeople.age = 1; 12 otherPeople.name = "test1"; 13 System.out.println(people.compareTo(people)); 14 System.out.println(people.compareTo(otherPeople)); 15 System.out.println(map.get(otherPeople)); 16 } 17 18 static class People implements Comparable{ 19 public int age; 20 public String name; 21 22 @Override 23 public int compareTo(Object o) { 24 if(o == null || !(o instanceof People)){ 25 return -1; 26 } 27 return ((People)o).age == age?0:-1; 28 } 29 } 30 }
结果:
1
0
-1
null
分析:hashmap比较hash值,而在map put进对象的时候 该hash值就已经固定了(详情参考HashMap内部类Entry<K,V>),所以put进之后如果改变与hashcode方法的计算有关系的属性时,hashcode()返回的变了,map里也就找不到了
而concurrentSkipListMap比较的是对象的compartTo()方法(concurrentSkipListMap的key必须实现Comparable接口),同一个对象,不管改变什么属性,改变之后自己跟自己compareTo返回的肯定是相等的,因为比较的都是改变之后的同一个对象,而不像hashmap一样,是执行put方法的时候获取的hashcode值与改变之后的值相比较。所以concurrentSkipListMap的key不管属性怎么变 get(对象本身)都能获取到。上段代码,最后一个返回null的原因是:因为people的age已经变成了2,也就是说concurrentSkipListMap中的该对象(key)的age也是2,而otherpeople的age是1, compareto()方法返回不是零。所以get的时候返回null。