今天遇到一个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相关:

  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 HashMap<People, Integer>();
  7. map.put(people, 1);
  8. people.age = 2;
  9. System.out.println("同一对象修改hashcode:" + map.get(people));
  10. People otherPeople = new People();
  11. otherPeople.age = 1;
  12. otherPeople.name = "test";
  13. System.out.println("不同对象有相同的hashcode与equals:" + map.get(otherPeople));
  14. }
  15.  
  16. static class People{
  17. public int age;
  18. public String name;
  19. @Override
  20. public int hashCode() {
  21. return age;
  22. }
  23.  
  24. @Override
  25. public boolean equals(Object obj) {
  26. if(obj == null || !(obj instanceof People)){
  27. return false;
  28. }
  29.  
  30. if(((People)obj).name != name){
  31. return false;
  32. }
  33. return true;
  34. }
  35. }
  36. }

结果:

同一对象修改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。

java HashMap HashSet的存储方式的更多相关文章

  1. 原码,补码,反码的概念及Java中使用那种存储方式

    原码,补码,反码的概念及Java中使用那种存储方式: 原码:原码表示法是机器数的一种简单的表示法.其符号位用0表示正号,用:表示负号,数值一般用二进制形式表示 补码:机器数的补码可由原码得到.如果机器 ...

  2. Java HashMap两种遍历方式

    第一种: Map map = new HashMap(); Iterator iter = map.entrySet().iterator(); while (iter.hasNext()) { Ma ...

  3. Java HashMap 四种遍历方式

    HashMap遍历方式包含以下4种: 1.遍历KeySet,再通过Key来getValue. 2.使用entrySet的迭代器. 3.foreach entrySet的方式. 3.foreache v ...

  4. Java集合 -- HashSet 和 HashMap

    HashSet 集合 HashMap 集合 HashSet集合 1.1 Set 接口的特点 Set体系的集合: A:存入集合的顺序和取出集合的顺序不一致 B:没有索引 C:存入集合的元素没有重复 1. ...

  5. Java中HashSet和HashMap

    Set中存储元素为什么不重复(即使hashCode相同)? HashSet中存放自定义类型元素时候,需要重写对象中的hashCode方法和equals方法, HashSet中存放自定义类型元素时候,需 ...

  6. Java 集合 HashMap & HashSet 拾遗

    Java 集合 HashMap & HashSet 拾遗 @author ixenos 摘要:HashMap内部结构分析 Java HashMap采用的是冲突链表方式 从上图容易看出,如果选择 ...

  7. Java List/HashSet/HashMap的排序

    在对Java无序类集合,如List(ArrayList/LinkedList).HashSet(TreeSet有序).HashMap等排序时,Java中一个公共的类Collections,提供了对Ja ...

  8. Redis入门 – Jedis存储Java对象 - (Java序列化为byte数组方式)

    Redis入门 – Jedis存储Java对象 - (Java序列化为byte数组方式) 原文地址:http://alanland.iteye.com/admin/blogs/1600685(欢迎转载 ...

  9. StoreType.java 存储方式

    StoreType.java 存储方式 http://injavawetrust.iteye.com package com.iteye.injavawetrust.miner; /** * 存储方式 ...

随机推荐

  1. 【远程重启】使用windows自带的shutdown命令远程重启服务器(测试不行,此文作废)

    net use \\IP \ipc$ "password" /user:"username" shutdown -r -m \\IP -t 0 -f 添加远程关 ...

  2. javascript模块化---requirejs

    requirejs是异步执行 为什么会出现模块化1.不定什么时候,自己就将全局变量改变了2.函数名的冲突3.依赖关系不好管理如果b.js依赖a.js那么b必须放在a的下面解决的办法1.自执行函数来包装 ...

  3. myeclipse引入工程后运行出错

    An internal error occurred during: Launching efax on Tomcat 7.x . 项目运行时报错 因为你项目建的时候用的是Tomcat5.x 服务器 ...

  4. 【转载】LCT题单

    本篇博客的题单转载自FlashHu大佬的博客:LCT总结--应用篇(附题单)(LCT). 关于\(LCT\)可以查看这篇博客:\(LCT\)入门. 这里面有些题解的链接是空链接,尚未补全. 维护链信息 ...

  5. CSS之常见文字样式整理

    常见文字样式 行高:line-height,当我i们将行高的大小设置成当前元素的高度时,可以实现当行文本在当前元素中垂直方向居中显示的效果 水平对齐方式:text-align:left|center| ...

  6. python之道12

    整理今天笔记,课上代码最少敲3遍. 用列表推导式做下列小题 过滤掉长度小于3的字符串列表,并将剩下的转换成大写字母 l = ['wusir', 'laonanhai', 'aa', 'b', 'tai ...

  7. 抽屉head部分,hover应用,鼠标放上变色

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  8. python 时间加8小时后的时间

    eta_temp = one['arrival'].encode('utf-8') fd = datetime.datetime.strptime(eta_temp, "%Y-%m-%dT% ...

  9. python 用requests请求,报SSL:CERTIFICATE_VERIFY_FAILED错误

    https://www.aliyun.com/jiaocheng/437481.html

  10. Linux入门-第七周

    1.编写脚本实现传入进程PID,查看对应进程/proc下CPU.内存指标. #!/bin/bash read -p "Input PID Value: " pid #读取PID进程 ...