HashMap源码分析及简单手写实现

一、什么是HashMap

  • Hash散列:将一个任意的长度通过某种(Hash算法)算法转换成一个固定的值。
  • Map:地图 x,y存储
  • 总结:通过HASH出来的值,然后通过这个值 值定位到这个map,然后value存储到这个map中
  • 疑问
    • 1.key可以为空吗?
      • Null当成一个key来存储
    • 2.如果hash key 重复了 value会覆盖吗?
      • 不会覆盖,而是将原来的值赋给oldValue,然后将新的值赋给value。原来的对象存放在entry对象的next里面, 例如:
        Map map = new HashMap();
        map.put("name", "zhangsan"); 
        //调用put方法时,若key没有重复,返回值为null;若重复,则为返回之前存储的值。
        map.put("name","wanger");  //此处会返回"zhangsan"
  • 3.HashMap什么时候扩容?
    – 执行put()方法的时候,阈值达到容量的3/4会进行扩容!
    – 扩容为偶数依次为: 16 2×16=32 2×32=62 2×64 ……
    – HashMap的table: 数组+链表 数据结构

二、源码分析

1.初始化参数介绍

  • int initCapacity (初始化数组大小,默认值为16)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  • int maximum_capacity(数组最大容量)
static final int MAXIMUM_CAPACITY = 1 << 30;
  • float loadFactor (负载因子,默认值为0.75f)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • table:初始化一个名为table的entry数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

2.put方法分析

   public V put(K key, V value) {
       if (key == null)
           return putForNullKey(value);
       //首先根据key值计算出HashCode值
       int hash = hash(key.hashCode());
       /*然后通过一个indexFor方法计算出此元素该存放于table数组的哪个位置, 再检测此table的此坐标位置的entry链是否存在此key或此key值,若存在 则更新此元素的value值*/
       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++;
       //添加当前的表项到i的位置
       addEntry(hash, key, value, i);  
       return null;
   }

3.get方法分析

    public V get(Object key) {
        //键为null的entry在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获取hash值,与put方法一致。
        int hash = (key == null) ? 0 : hash(key);
        // 同样通过位与运算获取Entry[] tables下标
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
                e != null;  
                e = e.next) {
            Object k;
            // 当匹配到hash值相同,key相同的Entry元素时,返回Entry对象的vlaue
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

4.扩容源码分析

    void resize(int newCapacity) {

        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //生成一个新的table数组(entry对象数组)
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        //根据新的Capacity和负载因子去生成新的临界值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }


/* 因为table数组的容量增加了,那么相应的table的length也增加了,那么之前存储的元素的位置 也就不一样了,比如之前的length是16,现在的length是32,那么hashCode模16和HashCode 模32的结果很有可能会不一样,所以就只有重新去计算新的位置,方法是遍历数组,再遍历数组上 的entry链*/
    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;
            }
        }
    }

5、总结

当负载因子较大时,去给table数组扩容的可能性就会少,所以相对占用内存较少(空间上较少),但是每条entry链上的元素会相对较多,查询的时间也会增长(时间上较多)。反之就是,负载因子较少的时候,给table数组扩容的可能性就高,那么内存空间占用就多,但是entry链上的元素就会相对较少,查出的时间也会减少。所以才有了负载因子是时间和空间上的一种折中的说法。所以设置负载因子的时候要考虑自己追求的是时间还是空间上的少。

注意:设置initCapacity的时候,尽量设置为2的幂,这样会去掉计算比initCapactity大,且为2的幂的数的运算。

三、手写实现

1.定义Map接口

public interface Map<K,V> {
    public V put(K k,V v);
    public V get(K k);

    public int size();
    //HashMap内部的Entry对象
    public interface Entry<K,V>{
        public K getKey();

        public V getValue();
    }
}

2.实现类HashMap

public class HashMap<K,V> implements Map<K, V> {

    private static int defaultLength = 16;
    private static float defaultLoader = 0.75f;
    private Entry<K,V>[] table = null;
    private int size = 0;

    public HashMap(int length, float loader) {
        defaultLength = length;
        defaultLoader = loader;
        table = new Entry[defaultLength];
    }

    public HashMap() {
        this(defaultLength,defaultLoader);
    }

    public V put(K k, V v) {
        size++;
        int index = hash(k);
        Entry<K,V> entry = table[index];
        if(entry == null){
            table[index] = newEntry(k,v,null);

        }else{
            table[index] = newEntry(k, v, entry);
        }
        return table[index].getValue();
    }

    public V get(K key) {
        int index = hash(key);
        if(table[index] == null){
            return null;
        }
        return find(key,table[index]);
    }

    public int size() {
        return size;
    }

    class Entry<K,V> implements Map.Entry<K, V>{
        K k;
        V v;
        Entry<K,V> next;

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

        public V getValue() {
            return v;
        }

    }

    //key->hash
    public int hash(K k){
        int m = defaultLength;
        int i = k.hashCode() % m;
        return i >= 0 ? i : -i;
    }

    private Entry<K, V> newEntry(K k, V v, Entry<K,V> next) {
        return new Entry<K,V>(k,v,next);
    }

    //根据key和table查找元素
    private V find(K key, Entry<K,V> entry) {
        if(key == entry.getKey() || key.equals(entry.getKey())){
            if(entry.next != null){
                System.out.println("oldValue:"+entry.next.getValue());
            }
            return entry.getValue();
        }
        else{
            if(entry.next != null){
                return find(key,entry.next);

            }
        }
        return null;
    }
}

四、不足之处(伸缩性角度)

1.伸缩性

每当hashmap扩容的时候需要重新去add entey对象 需要重新Hash,然后放入我们新的entry table数组里面

当我们知道HashMap需要存多少值(或值特别大的时候),最好指定他们的扩容大小,防止在put的时候进行多次再扩容

2.时间复杂度

时间复杂度与key是否冲突有关(理想状态为O(1)), hash算法决定了效率

    原文作者:zhangsuhui_csdn
    原文地址: https://blog.csdn.net/zhangsuhui_csdn/article/details/81623668
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞