今天遇到一个bug,简单的说就是把自定义对象作为key 存到HashMap中之后,经过一系列操作(没有remove操作)之后 用该对象到map中取,返回null。

然后查看了HashMap的源代码,get方法的核心代码如下:

1 final EntrygetEntry(Object key) {2 int hash = (key == null) ? 0: hash(key);3 for (Entry 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 returne;10 }11 return null;12 }

可以看出 hashmap在比较Key的时候,是先比较该key的hashcode. 返回entry条件是hashcode相同并且对象的地址或者equals方法比较相同。然后检查了出现bug的代码,因为重写了hashcode方法,然后修改了对象的属性(与hashcode计算相关)导致不太容易注意到的bug

验证HashMap的存储与hashCode,equals相关:

public classMapTest {public static voidmain(String[] args){
People people= newPeople();
people.age= 1;
people.name= "test";
Map map = new HashMap();
map.put(people,1);
people.age= 2;
System.out.println("同一对象修改hashcode:" +map.get(people));
People otherPeople= newPeople();
otherPeople.age= 1;
otherPeople.name= "test";
System.out.println("不同对象有相同的hashcode与equals:" +map.get(otherPeople));
}static classPeople{public intage;publicString name;
@Overridepublic inthashCode() {returnage;
}
@Overridepublic booleanequals(Object obj) {if(obj == null || !(obj instanceofPeople)){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 HashMapmap;2
3 publicHashSet() {4 map = new HashMap<>();5 }6
7 public booleanadd(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 b =findPredecessor(key);2 Node n =b.next;3 for(;;) {4 if (n == null)5 return null;6 Node 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 returnn;19 if (c < 0)20 return null;21 b =n;22 n =f;23 }24 }

以上代码第16行可看出:比较的时候是根据对象重写的compareTo方法来比较的,我们做个测试:

1 public classMapTest {2 public static voidmain(String[] args){3 People people = newPeople();4 people.age = 1;5 people.name = "test";6 Map map = new ConcurrentSkipListMap();7 map.put(people, 1);8 people.age = 2;9 System.out.println(map.get(people));10 People otherPeople = newPeople();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 implementsComparable{19 public intage;20 publicString name;21
22 @Override23 public intcompareTo(Object o) {24 if(o == null || !(o instanceofPeople)){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),所以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。