HashMap与Hashtable(一)

HashMap和Hashtable作为保存键值对的容器,都是使用一个Entry数组,Entry元素本身又是一个链式结构,所以实现数据结构相同,都是一个数组-链表形式。(Entry是Map接口的一个内部接口,HashMap实现了Map接口,HashMap的内部类Entry实现了Map接口的内部接口Entry)

HashMap

主要的属性:

<span style="font-family:KaiTi_GB2312;">static final int DEFAULT_INITIAL_CAPACITY = 16;//默认容量,2的整数次幂
static final int MAXIMUM_CAPACITY = 1 << 30;//默认最大容量,当构造map时给出了超出此大小,则使用该值
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子
transient Entry<K,V>[] table;//底层entry数组,声明为transient原理与ArrayList相同
transient int size;//entry键值对的个数
int threshold;//键值对的阈值,达到则扩容为2倍capacity
final float loadFactor;//负载因子
transient int modCount;//修改次数,迭代中用于检测发出快速失败</span></span>

HashMap实现有:

<span style="font-family:KaiTi_GB2312;">public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable</span>
此处实现了Serializable接口,表明对象可以进行序列化,Entry数组以transient修饰,表示对象序列化操作忽略关于Entry数组部分,HashMap类中存在writeObject和readObject方法,在执行序列化时由HashMap 对象本身处理Entry数组的序列化和反序列化。同ArrayList,参见ArrayList的remove、序列化

Map的Map.Entry结构:

<span style="font-family:KaiTi_GB2312;">static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
        Entry(int h, K k, V v, Entry<K,V> n) {//构造新元素、节点的参数为四个
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        .......
}</span>
Map中包含一个Entry<K,V>[]类型的 table 数组,每个元素为Entry类型,包含一个链表结构用于解决hash冲突

以Map的两个主要函数,put、get说明冲突:

<span style="font-family:KaiTi_GB2312;">public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);//如果key为null,执行专有的putForNullKey
        int hash = hash(key);//根据构造对象时的hashcode,重新计算hash值
        int i = indexFor(hash, table.length);//根据重新计算的hash值,找出对应的table的下标位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;//在table[i]的链表中,如果存在hash值相等并且key值相等的Entry,则覆盖其value值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);//如果在table[i]中不存在相等的Entry(key相等)
        return null;					//则在table[i]以头插的方式,添加Entry
    }</span>
如果执行put操作的key为null,则在put中执行putForNullKey:

<span style="font-family:KaiTi_GB2312;">private V putForNullKey(V value) {//如果key为null,则value值放在table数组下标为0的位置,从该函数可以看出
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {//键值对的value可以作为key的附属
            if (e.key == null) {		//put操作时不在乎value为何值
                V oldValue = e.value;	//如果table[0]已经存在key为null的Entry,则覆盖value,否则addEntry
                e.value = value;		//跟正常插入一样执行addEntry,只不过此时i的值为0,key为null
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);//如果table[0]的链表中没有key为null的节点,则插入key为null的节点
        return null;
    }</span>
如果当前key节点对应的Entry不存在,则无论key是否为null,都正常执行addEntry:

<span style="font-family:KaiTi_GB2312;">void addEntry(int hash, K key, V value, int bucketIndex) {//重新计算后的hash值,key,value,table下标
        if ((size >= threshold) && (null != table[bucketIndex])) {//已经存在的Entry个数size达到阈值并且table
            resize(2 * table.length);	//下标的此处的Entry不为null,说明了扩展table需要两个条件都满足
            hash = (null != key) ? hash(key) : 0;//扩展后重新计算该key的哈希值
            bucketIndex = indexFor(hash, table.length);//以及在新的table中的下标bucketIndex
        }

        createEntry(hash, key, value, bucketIndex);//无论是否扩展,hash、bucketIndex是否变化,照常添加新Entry
    }
void createEntry(int hash, K key, V value, int bucketIndex) {//添加新Entry,头插方式
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);//原来的头结点即table[bucketIndex],作为next
        size++;
    }</span>
关于resize,table数组长度变为两倍,即table数组的长度一直为2的整数次幂,参考indexFor:

<span style="font-family:KaiTi_GB2312;">static int indexFor(int h, int length) {
        return h & (length-1);
    }</span>
重新计算后的hash值h,与table数组长度减一值进行与运算,返回数组下标bucketIndex。

length作为2的整数次幂,length-1表示有效位都为1,进行与运算保证结果bucketIndex,始终处于length范围内。

计算数组下标Hashtable选择的是求余运算(为了对比,提前说一下Hashtable):

<span style="font-family:KaiTi_GB2312;">index = (hash & 0x7FFFFFFF) % tab.length;</span>
将重新计算后的hash值与0x7FFFFFFF相与,得出一个正数值,再对数组长度求余。选择与0x7FFFFFFF相与而不是使用Math.abs方式取正整数,一是因为计算效率高,二是Math.abs在值为Integer.MIN_VALUE时,得到的仍然是负数,对tab的数组长度求余,尽量选择tab.length为质数,Hashtable选择的数组长度始终为奇数。

<span style="font-family:KaiTi_GB2312;">int newCapacity = (oldCapacity << 1) + 1;</span>
在HashMap中,一直说hash值为重新计算后的哈希值,根据key的hashCode重新计算出hash:

<span style="font-family:KaiTi_GB2312;">final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {//如果为字符串,则返回字符串的自定义的哈希值
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        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);//返回重新计算后的哈希值
    }</span>
如果key为字符串,则直接利用字符串中定义的哈希值即可,String类或Integer等属于常量类,其中定义的哈希值在内容不相同的情况下不会重复,可以直接利用。

额外提一下:

在并发环境中容易发生死锁问题,避免死锁的四个条件的一个简单方式就是按顺序加锁,如果不能获得全部锁,则释放已有的锁,实现按顺序加锁的一个策略可以是将锁按照其哈希值排列,按照哈希值的大小进行获取,这也是为什么在转账过程中最好使用账号标志,作为一个字符串,只要账号不重复,则所有的操作可以按相同的顺序获取两个账号,实现按顺序加锁,以避免死锁。

从hash函数中可以看出,重新计算hash值是在获取对象本身的hashCode后进行了几次无符号右移,实现高位低位都参与到后续的对table数组长度减一的与运算中,避免出现高位不同,低位相同,而导致的散列冲突。

在resize函数中存在一个消耗性能的地方,就是transfer:

<span style="font-family:KaiTi_GB2312;">void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//遍历旧table数组
            while(null != e) {//遍历每个Entry形成的链表
                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;
            }
        }
    }</span>
重新计算i也就是之前的bucketIndex,然后头插方式赋予引用关系。

至于get、containsKey方法则是调用getEntry:

<span style="font-family:KaiTi_GB2312;">final Entry<K,V> getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key);//计算hash值
        for (Entry<K,V> e = table[indexFor(hash, table.length)];//遍历table[bucketIndex]
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }</span>
这里多说一句,根据get返回值并不能知道Map中是否包含对应的key,如果key不存在,则返回null,如果key存在但是对应value为null,返回仍为null,所以可以利用返回boolean类型的containsKey来判断。

总结:

HashMap作为键值对的集合,底层结构为一个数组加链表的形式实现,以拉链发解决散列冲突,table数组的长度为2的正整数次幂,实现中重新计算了key对象的hash值并利用与运算来定位bucketIndex,value可以作为key的附属,都可以为null,key为null时键值对形成的Entry处于table[0]处,put操作时如果key值计算出的bucketIndex相同,则遍历table[bucketIndex]的Entry链表,如果存在key值相同(e.key==key||key.equals(e.key)),则覆盖value,否则头插法插入新Entry节点。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值