• java HashMap HashSet的存储方式


    今天遇到一个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。

  • 相关阅读:
    远程桌面 终端服务器超出最大连接数的解决方法
    html a标签中调用js中的方法的方法
    英文励志歌曲经典珍藏
    远程桌面 习惯性注销连接,出事了
    小幽默
    MySQL用户权限
    MyBatis Like 模糊查询
    MVC3控制器方法获取Form数据方法
    MVC 3 Razor中的@helper 语法
    MVC3实现多个按钮提交
  • 原文地址:https://www.cnblogs.com/hithlb/p/3735940.html
Copyright © 2020-2023  润新知