hashMap求hashcode,hash碰撞等原理

269 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

为了深入了解、首先要看为什么重写equals方法还要重写hashcode? Object类有hashcode方法,hashcode底层是C语言写的。根据对象内存地址来的,转换成整数类型

定律:

  1. 如果两个对象的hashcode的值相等,内容值是不一定相等。
  2. 如果使用equals方法比较两个对象的内容值,相等的情况下,则两个对象的hashcode相等。
  3. equals在Object类中,默认采用==比较内存地址是否相等。(String类对equals方法和hashcode方法进行了重写)

hashMap1.7的底层实现

hashMap和hashTable的区别: hashMap线程不安全、允许key为空,放在数组为0的位置。 hashTable线程安全,key不允许为空。

  1. 基于arrayList集合实现,优点不需要考虑hash碰撞的问题。缺点从头开始遍历效率低,时间复杂度是O(n)
  2. 基于数组加链表方式(1.7)如果链表过长时间复杂度是O(n)
  3. 基于数组加链表加红黑树(1.8)如果链表过长时间复杂度是O(n),数组容量>=64&&链表长度超过8位,就使用红黑树时间复杂度O(logN)

HashMap类似于容器。使用Entry对象存放键值对。

  • 时间复杂度:
  • O(1):只需要查询一次,使用数组查找Array[1],查询效率非常高
  • O(n):循环从头查到尾,查询效率非常低
  • O(logN):平方,二叉树,红黑树。2的10次方,只需要查10次就能查到2的10次方的数据量

hashMap如果没有发生hash碰撞,时间复杂度就是O(1) hash碰撞问题:

hash碰撞是怎么产生的?hashcode相等,内容不相等。 解决方法:jdk1.7数组加单向链表的方式,同一链表内hashcode值相同,值不相同。如果从链表中取值,时间复杂度为O(n)

如果key存放null,值会放到数组的0位,如果0位有值的话,会放到链表中 hashMap所有数据遍历效率非常低,因为会去遍历所有的链表、红黑树。

image.png

//基于arraList
public class ArrayListHashMap<K,V> {

    private List<Entry<K,V>> entrys = new ArrayList<>();

    class Entry<K,V>{
        K k;
        V v;
        public Entry(K k,V v){
            this.k = k;
            this.v = v;
        }

    }

    public void put(K k,V v){
        entrys.add(new Entry<K,V>(k,v));
    }


    public V get(K k){
        for (Entry entry:
                entrys) {
            if(entry.k.equals(k)){
                return (V) entry.v;
            }
        }
        return null;
    }


    public static void main(String[] args) {
        ArrayListHashMap<Object, String> arrayListHashMap = new ArrayListHashMap<>();
        arrayListHashMap.put("a","a");
        arrayListHashMap.put(97,"97");
        System.out.println(arrayListHashMap.get("a"));
        System.out.println(arrayListHashMap.get(97));
    }

}
//基于数组加链表
public class ExtHashMap<K,V> {
    //hashMap初始容量16
    private Entry[] entrys = new Entry[10000];

    class Entry<K,V>{
        K k;
        V v;
        Entry<K,V> next;
        public Entry(K k,V v){
            this.k = k;
            this.v = v;
        }
    }



    public void put(K k,V v){
        //如果key等于null,则放到0位
        int index = k == null ? 0 : k.hashCode() % entrys.length;
        //判断key是否发生冲突
        Entry oldEntry = entrys[index];
        if(oldEntry == null){

            //没有发生hash碰撞,直接放到数组中

            entrys[index] = new Entry<K,V>(k,v);

        }else{
            oldEntry.next = new Entry<K,V>(k,v);
        }
    }


    public V get(K k){
        int index = k == null ? 0 : k.hashCode() % entrys.length;
        for(Entry oldEntry = entrys[index];oldEntry != null;oldEntry = oldEntry.next ){
            if((k == null && oldEntry.k == null)||oldEntry.k.equals(k) ){
                return (V) oldEntry.v;
            }
        }
        return (V) entrys[index].v;
    }



    public static void main(String[] args) {
        ExtHashMap<Object, String> extHashMap = new ExtHashMap<>();
        extHashMap.put("a","a");
        extHashMap.put(97,"97");
        extHashMap.put(98,"98");
        extHashMap.put(null,"123");
        //a的hashcode和97的hashcode都是97,会出现hashcode碰撞
        //hashMap如果没有发生hash碰撞,时间复杂度就是O(1)
        System.out.println(extHashMap.get(97));
        System.out.println(extHashMap.get(98));
        System.out.println(extHashMap.get(null));
    }
}

计算hash得出index key.hashcode()%entry.length冲突会非常大,放到链表和红黑树,时间复杂度O(n)和O(logN)。 降低hash冲突,均匀的放到数组下标位置,时间复杂度O(1)

//求hash值源码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

核心代码两行(必须理解透):

(h = key.hashCode()) ^ (h >>> 16);  //计算hash值
i = (n - 1) & hash  //得到数组的坐标

n-1,因为每次扩容,2的幂次方,都是偶数,二进制都是100000。。。,1开头000。。。结束,二进制只有一个1,冲突概率非常大,减一变成奇数后,二进制产生1的概率就增大,11得1就会增加,再去与运算,减少hash冲突