HashMap(JDK1.7实现)

JDK1.7 数组+链表

JDK1.8 数组+链表+红黑树

解决Hash冲突方法:

  • 链表方法

0、小结

HashMap内部哟一个哈希表,即数组table,每个元素table[i]指向一个单项链表,根据键计算出hash值,取模得到数组中的索引位置bucketIndex,然后操作table[bucketIndex]指向的单向链表。

计算hash值

获取索引位置 tableIndex

操作table[TABLEIndex]指向的单向链表

存取的时候依据键的hash值,只在对应的链表中操作,不会访问别的链表,在对应链表操作时也是先比较hash值,如果相同再用equals方法比较。这就要求,相同对象棋hashCode返回值必须相同,如果是键自定义的类就特别注意这一点

在JDK1.8中做了优化,一定要注意-=

线程不安全*

1、HashMap jdk7内部组成

	//默认的Hash桶的大小,必须为2的幂次
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //哈希桶最大值
    static final int MAXIMUM_CAPACITY = 1 << 30;

	//负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;


  
    //空表
    static final Entry<?,?>[] EMPTY_TABLE = {};

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */
    //table是一个Entry类型的数组,称为哈希表或哈希桶,
    // 其中每个元素指向一个单项链表,链表中的每个节点表示表示一个键值对
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

    /**
     * The number of key-value mappings contained in this map.
     */
    //表示实际键值对的个数
    transient int size;

    /**
     * The next size value at which to resize (capacity * load factor).
     * @serial
     */
    // If table == EMPTY_TABLE then this is the initial capacity at which the
    // table will be created when inflated.
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;

  
    transient int modCount;

    /**
     * The default threshold of map capacity above which alternative hashing is
     * used for String keys. Alternative hashing reduces the incidence of
     * collisions due to weak hash code calculation for String keys.
     * <p/>
     * This value may be overridden by defining the system property
     * {@code jdk.map.althashing.threshold}. A property value of {@code 1}
     * forces alternative hashing to be used at all times whereas
     * {@code -1} value ensures that alternative hashing is never used.
     */
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

Entry<K,V>[] table: table是一个Enmtrt类型的数组,称为哈希表或者哈希桶,其中每个元素指向一个单项链表,链表中的每个节点表示一个键值对;Entry是内部类

threshold:哈希表扩展的阈值;计算方式=table.length*负载因子当键值对的个数size大于等于threshold时考虑进行扩展

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key; //key
        V value;//值
        Entry<K,V> next;//下一个节点
        int hash;//hash值

2、构造方法

2.1、public HashMap()

//默认容量16,负载因子0.75
public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

2.2、public HashMap(int initialCapacity) {

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

2.3、public HashMap(int initialCapacity, float loadFactor)

3、保存键值对 put(key,value)

允许key与value为null

  • 计算键的哈希值
  • 根据哈希值得到保存位置(取模)
  • 插到对应位置的链表头部(若该链表没有值)或更新已有值
  • 根据需要扩展table大小
public V put(K key, V value) {
        //如果是第一次保存,首先调用 inflateTable()方法给table分配实际的空间
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //检查key是否为null,如果是,调用putForNullKey单独处理
        if (key == null)
            return putForNullKey(value);
    
        //key不为null的情形
        //计算hash的值,基于key自身的hashCode方法的返回值又进行了一些位运算,目的是随机和均匀性
        int hash = hash(key);
        int i = indexFor(hash, table.length);//计算将这个键值对放到table的哪个位置

        //在链表中逐个查找是否已经有这个键了
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //比较的时候,先比较hash值,hash相同的时候,再使用equals方法进行比较
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                //如果找到直接修改Entry中的value值即可
                e.recordAccess(this);
                return oldValue;
            }
        }

        //modCount++的含义与ArrayList和LinkedList中介绍一样,为记录修改次数,方便在迭代中检测结构性变化
        modCount++;
        //如果没找到,调用addEntry方法在给定的位置添加数据
        addEntry(hash, key, value, i);
        return null;
    }

//

///inflateTable方法//
private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize

        //默认情况下,capacity的值为16,threshold变为12,table会分配一个长度为16的Entry的数组
        int capacity = roundUpToPowerOf2(toSize);
        //阈值=capacity*3/4
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }




/hash方法//
  final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }


///indeFor(方法///
//调用indexFor方法,计算应该将这个键值对放到table的哪个位置
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        //HashMap中,length为2的幂次方,h&(length-1)等价于求模运算 h%length
        return h & (length-1);//等价于进行取余操作
    }



addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
        //加入size已经要超过阈值threshold了,并且对应的table位置与已经插入过对象了
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //调用resize方法对table进行扩展,扩展策略是乘2
            resize(2 * table.length);
			
            hash = (null != key) ? hash(key) : 0;

            bucketIndex = indexFor(hash, table.length);
        }

        //如果是空间够的,不需要resize,则调用createEntry方法添加。
        createEntry(hash, key, value, bucketIndex);

    }


///resize该方法///
    void resize(int newCapacity) {
        //记录旧的表
        Entry[] oldTable = table;
        //旧的表的长度
        int oldCapacity = oldTable.length;

        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];

        //调用transfer方法将原来的键值对移植过来
        transfer(newTable, initHashSeedAsNeeded(newCapacity));

        table = newTable;
        //计算新的阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }



//这段代码遍历原来的每个键值对,计算新位置,并保存到新位置
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }




 void createEntry(int hash, K key, V value, int bucketIndex) {
        //新建一个Entry对象,插入单向链表的头部,并增加size;注意与JDK1.8的区别
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

4、得到值 get(key)

  • 计算键的hash值
  • 根据hash找到table中的对应链表
  • 在链表中查找
  • 逐个标胶,先通过hash快速比较,hash相同再通过equas比较
//HashMap支持key为Null,key为null的时候放在table[0],调用getForNullKey()获取值
    public V get(Object key) {
        //key为null时,
        if (key == null)
            return getForNullKey();

        //获取Entry
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }


final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        //计算hash值
        int hash = (key == null) ? 0 : hash(key);

        //根据hash值找到table中的对应链表
        //在链表中遍历查找
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //逐个比较,先通过hash快速比较,hash相同再通过equals比较
            if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }



5、删除键值对

public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }



//找到在数组中的位置遍历链表删除
    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        //计算hash值
        int hash = (key == null) ? 0 : hash(key);
        //获取该值的索引位置
        int i = indexFor(hash, table.length);
        //遍历table[i],查找待删节点,使用prev指向前一个节点,next指向后一个接亲
        //e指向当前节点
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            //找到了,删除
            if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }



面试题

1、为什么初始化数组大小为2的幂次方》?

16 32

方便进行与操作(等价于取余操作,加速)_

2、为什么hashcode之后进行位移运算?

h: 0101 0101

15: 0000 1111

使得hashCode的高位参与运算,避免hash值过于集中

解决哈希冲突的方法

解决哈希冲突的方法一般有:开放定址法、链地址法(拉链法)、再哈希法、建立公共溢出区等方法。

2.1 开放定址法

从发生冲突的那个单元起,按照一定的次序,从哈希表中找到一个空闲的单元。然后把发生冲突的元素存入到该单元的一种方法。开放定址法需要的表长度要大于等于所需要存放的元素。
在开放定址法中解决冲突的方法有:线行探查法、平方探查法、双散列函数探查法。
开放定址法的缺点在于删除元素的时候不能真的删除,否则会引起查找错误,只能做一个特殊标记。只到有下个元素插入才能真正删除该元素。

v2.1.1 线行探查法

线行探查法是开放定址法中最简单的冲突处理方法,它从发生冲突的单元起,依次判断下一个单元是否为空,当达到最后一个单元时,再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止。
可以参考csdn上flash对该方法的演示:
http://student.zjzk.cn/course_ware/data_structure/web/flash/cz/kfdzh.swf

2.1.2 平方探查法

平方探查法即是发生冲突时,用发生冲突的单元d[i], 加上 1²、 2²等。即d[i] + 1²,d[i] + 2², d[i] + 3²…直到找到空闲单元。
在实际操作中,平方探查法不能探查到全部剩余的单元。不过在实际应用中,能探查到一半单元也就可以了。若探查到一半单元仍找不到一个空闲单元,表明此散列表太满,应该重新建立。

2.1.3 双散列函数探查法

这种方法使用两个散列函数hl和h2。其中hl和前面的h一样,以关键字为自变量,产生一个0至m—l之间的数作为散列地址;h2也以关键字为自变量,产生一个l至m—1之间的、并和m互素的数(即m不能被该数整除)作为探查序列的地址增量(即步长),探查序列的步长值是固定值l;对于平方探查法,探查序列的步长值是探查次数i的两倍减l;对于双散列函数探查法,其探查序列的步长值是同一关键字的另一散列函数的值。

2.2 链地址法(拉链法)

链接地址法的思路是将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。
如下一组数字,(32、40、36、53、16、46、71、27、42、24、49、64)哈希表长度为13,哈希函数为H(key)=key%13,则链表法结果如下:

0
1 -> 40 -> 27 -> 53
2
3 -> 16 -> 42
4
5
6 -> 32 -> 71
7 -> 46
8
9
10 -> 36 -> 49
11 -> 24
12 -> 64
注:在java中,链接地址法也是HashMap解决哈希冲突的方法之一,jdk1.7完全采用单链表来存储同义词,jdk1.8则采用了一种混合模式,对于链表长度大于8的,会转换为红黑树存储。

2.3 再哈希法

就是同时构造多个不同的哈希函数:
Hi = RHi(key) i= 1,2,3 … k;
当H1 = RH1(key) 发生冲突时,再用H2 = RH2(key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。

2.4 建立公共溢出区

将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值