HashMap分析

来源:互联网 发布:ajax传递数组 编辑:程序博客网 时间:2024/05/16 17:33

参考:http://www.cnblogs.com/chengxiao/p/6059914.html

HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,急需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值

initialCapacity默认为16,loadFactory默认为0.75

我们看下其中一个

public HashMap(int initialCapacity, float loadFactor) {     //此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(230)        if (initialCapacity < 0)            throw new IllegalArgumentException("Illegal initial capacity: " +                                               initialCapacity);        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        if (loadFactor <= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException("Illegal load factor: " +                                               loadFactor);        this.loadFactor = loadFactor;        threshold = initialCapacity;             init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现    }

从上面这段代码我们可以看出,在常规构造器中,没有为数组table分配内存空间(有一个入参为指定Map的构造器例外),而是在执行put操作的时候才真正构建table数组。

put方法:

    public V put(K key, V value) {        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)        if (table == EMPTY_TABLE) {            inflateTable(threshold);        }       //如果key为null,存储位置为table[0]或table[0]的冲突链上        if (key == null)            return putForNullKey(value);        int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀        int i = indexFor(hash, table.length);//获取在table中的实际位置        for (Entry<K,V> e = table[i]; e != null; e = e.next) {        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value            Object k;            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败        addEntry(hash, key, value, i);//新增一个entry        return null;    }   

先来看看inflateTable这个方法

private void inflateTable(int toSize) {        int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1        table = new Entry[capacity];        initHashSeedAsNeeded(capacity);    }

inflateTable这个方法用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.

再来看看addEntry的实现:

void addEntry(int hash, K key, V value, int bucketIndex) {        if ((size >= threshold) && (null != table[bucketIndex])) {            resize(2 * table.length);//当size超过临界阈值threshold,并且即将发生哈希冲突时进行扩容            hash = (null != key) ? hash(key) : 0;            bucketIndex = indexFor(hash, table.length);        }        createEntry(hash, key, value, bucketIndex);    }

get方法

 public V get(Object key) {     //如果key为null,则直接去table[0]处去检索即可。        if (key == null)            return getForNullKey();        Entry<K,V> entry = getEntry(key);        return null == entry ? null : entry.getValue(); }
final Entry<K,V> getEntry(Object key) {        if (size == 0) {            return null;        }        //通过key的hashcode值计算hash值        int hash = (key == null) ? 0 : hash(key);        //indexFor (hash&length-1) 获取最终数组索引,然后遍历链表,通过equals方法比对找出对应记录        for (Entry<K,V> e = table[indexFor(hash, table.length)];             e != null;             e = e.next) {            Object k;            if (e.hash == hash &&                 ((k = e.key) == key || (key != null && key.equals(k))))                return e;        }        return null;    }  

可以看出,get方法的实现相对简单,key(hashcode)–>hash–>indexFor–>最终索引位置,找到对应位置table[i],再查看是否有链表,遍历链表,通过key的equals方法比对查找对应的记录。要注意的是,有人觉得上面在定位到数组位置之后然后遍历链表的时候,e.hash == hash这个判断没必要,仅通过equals判断就可以。其实不然,试想一下,如果传入的key对象重写了equals方法却没有重写hashCode,而恰巧此对象定位到这个数组位置,如果仅仅用equals判断可能是相等的,但其hashCode和当前对象不一致,这种情况,根据Object的hashCode的约定,不能返回当前对象,而应该返回null,后面的例子会做出进一步解释。

原创粉丝点击