Java记录 -71- HashMap源码剖析

来源:互联网 发布:mysql float 转换int 编辑:程序博客网 时间:2024/06/05 11:21

HashMap底层维护着一个数组,我们向HashMap中所放置的对象实际上是存储在该数组当中。数组中每个元素都维护着一个链表,每个链表中的所有元素的hash值都一样。

HashMap的四个构造方法:

  1. 底层维护的数组 Entry[] table,数组默认大小为16,并为2的整数次幂;

  2. 底层数组存放的Entry对象,是HashMap的一个内部类,包含了key,value和next变量;

   /**    * The table, resized as necessary. Length MUST Always be a power of two.    */   transient Entry[] table;   static class Entry<K,V> implements Map.Entry<K,V> {        final K key;        V value;        Entry<K,V> next;        final int hash;        ......    }       public HashMap(int initialCapacity, float loadFactor) {        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);        // Find a power of 2 >= initialCapacity        int capacity = 1;        while (capacity < initialCapacity)            capacity <<= 1;        this.loadFactor = loadFactor;        threshold = (int)(capacity * loadFactor);        table = new Entry[capacity];        init();    }    public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);    }    public HashMap() {        this.loadFactor = DEFAULT_LOAD_FACTOR;        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);        table = new Entry[DEFAULT_INITIAL_CAPACITY];        init();    }    public HashMap(Map<? extends K, ? extends V> m) {        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);        putAllForCreate(m);    }


下面来看做关心的添加方法put:

  1. 当添加的key为null时,会取出table的第一个元素,并在该元素所领导的链表中寻找key==null的元素,有则替换,没有则添加;

  2. key不为null时,需要计算key的hash值,调用一个函数式,根据经验计算出一个整数值来;

  3. 然后再根据计算出的hash值和数组table的长度计算出新添加元素在数组中的位置;

  4. 从底层数组中取出上面计算的位置的元素值,即Entry对象,从该对象领导的链表中寻找key相等的对象,有则替换;

  5. 如果上一步没有找到相等的对象,则添加新对象;将对象添加到底层数组table中,位置为上面计算得到的位置值,并将该对象的next指向该位置以前存放的元素;

public V put(K key, V value) {        if (key == null)            return putForNullKey(value);        int hash = hash(key.hashCode());        int i = indexFor(hash, table.length);        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            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++;        addEntry(hash, key, value, i);        return null;    }    private V putForNullKey(V value) {        for (Entry<K,V> e = table[0]; e != null; e = e.next) {            if (e.key == null) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;        addEntry(0, null, value, 0);        return null;    }    static int hash(int h) {        // 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);    }    static int indexFor(int h, int length) {        return h & (length-1);    }    void addEntry(int hash, K key, V value, int bucketIndex) {        Entry<K,V> e = table[bucketIndex];        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);        if (size++ >= threshold)            resize(2 * table.length);    }

 所有集合存放元素就是为了读取使用,下面来看看取元素:

  1. get方法key为null时需要特殊处理,即从底层数组table的第一个元素中寻找key==null的值返回;

  2. 如果key不为null,则需要计算key的hash值,根据该值在table中取得一个元素,然后再在该元素所在的链表中寻找对应key的值返回;

    public V get(Object key) {        if (key == null)            return getForNullKey();        int hash = hash(key.hashCode());        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.equals(k)))                return e.value;        }        return null;    }    private V getForNullKey() {        for (Entry<K,V> e = table[0]; e != null; e = e.next) {            if (e.key == null)                return e.value;        }        return null;    }


常用的方法还有:

containsKey,remove,keySet,entrySet,values

这些方法都需要先在底层数组中找,然后遍历对应的链表。


根据设定的阀值,但达到阀值时,HashMap的底层数组将进行扩容。

 

新加的对象为什么添加到链的头部,而不是尾部?

为了提高性能,操作系统中有这么一个原则,认为最新添加到链中的元素下次访问的几率要大于以前的;因此添加到链表头部,如果下次访问该元素时就可以第一个访问到,设想如果放到尾部则需要变量整个链表来找到该元素。


HashMap添加一个对象的思路过程:

当向HashMap中put一对键值时,它会根据key的hashCode值计算出一个位置,该位置就是此对象准备往数组中存放的位置。

如果该位置没有对象存在,就将此对象直接放进数组当中;

如果该位置已经有对象存在了,则顺着此存在的对象的链开始寻找(Entry类有一个Entry类型的next成员变量,指向了该对象的下一个对象),如果此链上有对象的话,再去使用equals方法进行比较;

如果对此链上的某个对象的key比较为true,则证明已经存在该对象,此时将用新对象替换旧对象;

如果对此链上的所有对象的equals方法比较都为false,则将该对象放到数组当中,然后将数组中该位置以前存在的那个对象链接到此对象的后面。

原创粉丝点击