其他分享
首页 > 其他分享> > ArrayMap跟HashMap区别

ArrayMap跟HashMap区别

作者:互联网

Hash碰撞的解决方式

ArrayMap: 开放地址法

  1. 首先查看构造函数
 public ArrayMap() { 
        this(0, false);
    }
 public ArrayMap(int capacity) {
        this(capacity, false);
    }
/**
* identityHashCode: 是否使用系统的System.identityHashCode(key),跟对象存储位置地址值相关 , 
* false时使用对象的hashCode,如果重写就使用对象的hashCode方法(String 重写了,所以map键常用的字符串是一致的),未重写则使用object的hashcode
* 同系统的调用同一个本地方法,结果值一致
*/
public ArrayMap(int capacity, boolean identityHashCode) {
        mIdentityHashCode = identityHashCode;

        // 这是同HashMap不同的地方,如果当前没有设置大小默认为0,hashMap默认数组为16,节约空间了吧
        if (capacity < 0) {
            mHashes = EMPTY_IMMUTABLE_INTS;
            mArray = EmptyArray.OBJECT;
        } else if (capacity == 0) {
            mHashes = EmptyArray.INT;
            mArray = EmptyArray.OBJECT;
        } else {
            // 初始化值,根据提供的大小,但同HashMap会改变(注意并非指定多少即为多大空间数组)
            allocArrays(capacity); 
        }
        mSize = 0;
}
  1. 搞懂几个数据含义
  1. 查看 put()方法
    @Override
    public V put(K key, V value) {
        final int osize = mSize;
        final int hash;
        int index;
        if (key == null) { //key是空,则通过indexOfNull查找对应的index,支持存储null键
            hash = 0;
            index = indexOfNull();
        } else { //否则 通过key查找下标index 
            hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode(); //构造函数中指定使用哪种hash值
            index = indexOf(key, hash); //稍后解释(根据hash值及key找到对应的下标index ,存在为正,不然为 ~位置 一个负数值)
        }
        if (index >= 0) { //找到了当前存储的key键在mArray中替换成新值value并返回旧值
            index = (index<<1) + 1;
            final V old = (V)mArray[index];
            mArray[index] = value;
            return old;
        }

        index = ~index; //将indexOf中找到的~index在次~即为(~~index) == index一个正值,即为数据将要插入的位置
        if (osize >= mHashes.length) { //判断是否需要扩容大小,扩容为默认 4 ->8 -> 12 -> 18(不足向上取,比如初始7个数据则数组大小为8),扩容后赋值并回收原有数组
            final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                    : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

            if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
            //将原来的数组赋值保存后在使用allocArrays(n)对原来数组根据新大小n进行扩容
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            allocArrays(n); //具体的扩容函数,创建一个新的数据,可能存在缓存数组,稍后讲解

            if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) { //对于两个线程同时扩容报错,同HashMap一致是非线程安全
                throw new ConcurrentModificationException();
            }

            if (mHashes.length > 0) { 
                //将上方保存的原有数据拷贝到新数组中,注意下标从0开始及大小原有数组长度
                if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
                System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
                System.arraycopy(oarray, 0, mArray, 0, oarray.length);
            }
            //释放原有数组,已经在上方拷贝过了,这里涉及到缓存数据
            //查看是否数据还有利用价值,其后会讲到,大致是对数据量为4或者8做一个缓存以便其后还会利用到
            //如果数据量超过8就就没有必要占用内存去缓存它了
            freeArrays(ohashes, oarray, osize);
        }

        if (index < osize) {  //index是指当前数据key的hash值下标即key应该插入数组的位置不是最后一位则移动他将插入位置及其后的所有数据腾出位置给其插入使用
            if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
                    + " to " + (index+1));
            //注意: 将mHashes的index 位置以后的向后移动一位,同时mArray也是同时向后移动2位,给key.hash,跟(key,value)插入让出位置
            System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
            System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
        }

        if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
            if (osize != mSize || index >= mHashes.length) { //再次判断是否线程安全
                throw new ConcurrentModificationException();
            }
        }
        mHashes[index] = hash; //在特定位置插入mHashes,及mArray数组上
        mArray[index<<1] = key;
        mArray[(index<<1)+1] = value;
        mSize++; //数据值+1
        return null;
    }
  1. 初始化数组函数 allocArrays(capacity)用于数组的初始化及缓存数据
    private void allocArrays(final int size) {
        if (mHashes == EMPTY_IMMUTABLE_INTS) {
            throw new UnsupportedOperationException("ArrayMap is immutable");
        }
        //设置缓存用的,当数据量由24 降为 8时不用再次重新创建加载,而是直接使用mTwiceBaseCache缓存的数组即可
        if (size == (BASE_SIZE*2)) {
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCache != null) { //缓存 8数据存在 
                    final Object[] array = mTwiceBaseCache;
                    mArray = array; 
                    mTwiceBaseCache = (Object[])array[0]; 
                    mHashes = (int[])array[1]; 
                    array[0] = array[1] = null;
                    mTwiceBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                            + " now have " + mTwiceBaseCacheSize + " entries");
                    return;
                }
            }
        } else if (size == BASE_SIZE) {
            synchronized (ArrayMap.class) {
                if (mBaseCache != null) {
                    final Object[] array = mBaseCache;
                    mArray = array;
                    mBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                            + " now have " + mBaseCacheSize + " entries");
                    return;
                }
            }
        }

        mHashes = new int[size];
        mArray = new Object[size<<1];
    }
  1. 对于查找indexOf(key,hash)

    int indexOf(Object key, int hash) {
        final int N = mSize;

        // Important fast case: if nothing is in here, nothing to look for.
        if (N == 0) {
            return ~0;
        }
        // 使用二分查找到当前hash值在mHashes数组中的下标,如果不存在返回(~当前需要插入的位置)后续在~即为正在需要插入的位置 (~~A == A)
        int index = binarySearchHashes(mHashes, N, hash);

        if (index < 0) { //如果没有找到就返回一个负值
            return index;
        }

        //如果当前index下标的key同查找的一致就返回
        if (key.equals(mArray[index<<1])) {
            return index;
        }

        // 先向上查找相同hash值是否key一致,否则向下查找
        int end;
        for (end = index + 1; end < N && mHashes[end] == hash; end++) {
            if (key.equals(mArray[end << 1])) return end;
        }
       
        for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
            if (key.equals(mArray[i << 1])) return i;
        }

        // 如果都没有找到,将返回需要插入位置的负值,注意这里是相同hash值的最后一个其后添加数据
        return ~end;
    }
  1. remove 方法:
public V removeAt(int index) {
        final Object old = mArray[(index << 1) + 1]; 
        final int osize = mSize; //msize是当前存储实际数据大小,mHashes.length为当前申请的数组大小
        final int nsize;
        if (osize <= 1) {
            // 当实际数据为1时,即移除以后恢复成默认值null
            if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            mHashes = EmptyArray.INT;
            mArray = EmptyArray.OBJECT;
            freeArrays(ohashes, oarray, osize);
            nsize = 0;
        } else {
            nsize = osize - 1; 
            // 如果当前数组大于 8并且实际数据量小于 数组长度的 1/3 则重新分配内存空间
            if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
                //重新分配后实际数组的大小
                final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);

                if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
                //备份数据以便后期复制使用
                final int[] ohashes = mHashes;
                final Object[] oarray = mArray;
                allocArrays(n); //重新分配数据,注意里面会用到已经缓存的4和8的数据

                if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) { //判断是否线程安全
                    throw new ConcurrentModificationException();
                }
                //移除index上的数据分两个部分移除,首先复制0--index大小的数据就是到 (index -1)处
              
                if (index > 0) {
                    if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
                    System.arraycopy(ohashes, 0, mHashes, 0, index);
                    System.arraycopy(oarray, 0, mArray, 0, index << 1);
                }
                //  再次从index + 1 处移动nsize - index的数据复制到新数组中
                if (index < nsize) {
                    if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + nsize
                            + " to " + index);
                    System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
                    System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
                            (nsize - index) << 1);
                }
            } else { //不用重新分配内存,直接可用的则直接移动其后的数据,并将最后一位设置成默认null
                if (index < nsize) {
                    if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
                            + " to " + index);
                    System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
                    System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
                            (nsize - index) << 1);
                }
                mArray[nsize << 1] = null;
                mArray[(nsize << 1) + 1] = null;
            }
        }
        if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
            throw new ConcurrentModificationException();
        }
        mSize = nsize; //减少一位并返回删除的旧值
        return (V)old;
    }
  1. 重点关注缓存 freeArrays(final int[] hashes, final Object[] array, final int size) , 结合 allocArrays(int size)同看
    • 注意参数的内容 : hashes 跟 array的长度关系是 1/ 2 , 切记切记啊
    private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
        if (hashes.length == (BASE_SIZE*2)) { //如果当前缓存的大小为8
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCacheSize < CACHE_SIZE) {
                    array[0] = mTwiceBaseCache; //第一次的array[0] = null, 但是其后的将会是一个链表结构这一次缓存中的array[0] ->指向的是上一次缓存的整体数字,但是array的中长度依然是 16 ,最多缓存 10个
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mTwiceBaseCache = array;
                    mTwiceBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
                            + " now have " + mTwiceBaseCacheSize + " entries");
                }
            }
        } else if (hashes.length == BASE_SIZE) { //同上所述不过缓存的数据为4,array的总长度为8,最多缓存10个
            synchronized (ArrayMap.class) {
                if (mBaseCacheSize < CACHE_SIZE) {
                    array[0] = mBaseCache;
                    array[1] = hashes;
                    for (int i=(size<<1)-1; i>=2; i--) {
                        array[i] = null;
                    }
                    mBaseCache = array;
                    mBaseCacheSize++;
                    if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
                            + " now have " + mBaseCacheSize + " entries");
                }
            }
        }
    }
  1. 再次查看 allocArrays

    private void allocArrays(final int size) {
        if (mHashes == EMPTY_IMMUTABLE_INTS) {
            throw new UnsupportedOperationException("ArrayMap is immutable");
        }
        if (size == (BASE_SIZE*2)) { //8数据同下
            synchronized (ArrayMap.class) {
                if (mTwiceBaseCache != null) {
                    final Object[] array = mTwiceBaseCache;
                    mArray = array;
                    mTwiceBaseCache = (Object[])array[0];
                    mHashes = (int[])array[1];
                    array[0] = array[1] = null;
                    mTwiceBaseCacheSize--;
                    if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes
                            + " now have " + mTwiceBaseCacheSize + " entries");
                    return;
                }
            }
        } else if (size == BASE_SIZE) { //如果当前缓存的有数据,则array是一个大小为8的链式结构,加入缓存了多次,但是被复用以后其被赋值给mArray的时候依然是一个大小为8且在后续会被重新赋值的,所以不用担心链式多层结构问题
            synchronized (ArrayMap.class) {
                if (mBaseCache != null) {
                    final Object[] array = mBaseCache;
                    mArray = array;
                    mBaseCache = (Object[])array[0];  //将下一层缓存的数据重新赋值给4个缓存
                    mHashes = (int[])array[1]; //当前为4的数组赋值给mHashes
                    array[0] = array[1] = null; 
                    mBaseCacheSize--; //缓存池中数量减1
                    if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes
                            + " now have " + mBaseCacheSize + " entries");
                    return;
                }
            }
        }

        mHashes = new int[size];
        mArray = new Object[size<<1];
    }
  1. 特别解释,为何最大缓存数据是10层:

SparseArray

//成员变量分析
public class SparseArray<E> implements Cloneable {
    private static final Object DELETED = new Object(); //数组中的数据如果被删除,可能仅仅标记value为DELETED并不会每次都重新移动数组,太消耗时间了,同时可以直接在Deleted位置上put数据覆盖之
    private boolean mGarbage = false; //是否需要垃圾回收,即value中存在DELETED,但并不一定会被回收的,在delete(key)时被标记为Deleted
    private int[] mKeys; //一个key对应一个value,且key是int型不可重复,多在从0-N有序列使用,比如RecycleView中的viewType就是有序且不可重复的,缓存即使用SparseArray
    private Object[] mValues;
    private int mSize;
 public SparseArray() { //默认keys和values数组长度为10
        this(10);
    }
public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //如果找到i则i对应的value数组位置修改值
        if (i >= 0) {
            mValues[i] = value;
        } else {
            i = ~i; //没有找到,这个只是根据二分查找到第一个大于i的位置插入这里,其后值向后移动

            if (i < mSize && mValues[i] == DELETED) { //如果当前位置被标记为Deleted删除标记,表示该位置可用
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
            //有Deleted则mGarbage = true,数组是可以靠移动而不是扩容就能容纳该put新值的
            if (mGarbage && mSize >= mKeys.length) {
                gc(); //新gc将deleted标志都删除掉
 
                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            //如果位置没有Deleted,且数组已经满了,则二倍的扩容
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }
private void gc() {
        // Log.e("SparseArray", "gc start with " + mSize);
        int n = mSize;
        int o = 0;
        int[] keys = mKeys;
        Object[] values = mValues;
        for (int i = 0; i < n; i++) {
            Object val = values[i];
            if (val != DELETED) {
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
                o++;
            }
        }
        mGarbage = false;
        mSize = o;
        // Log.e("SparseArray", "gc end with " + mSize);
    }
public E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
            return (E) mValues[i];
        }
    }

    /**
     * Removes the mapping from the specified key, if there was any.
     */
    public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

HashMap

  1. 数据结构为:HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体;

    • 根据使用场景设置初始容量及负载因子
    • hashMap实现了Map接口,使用键值对进行映射,map中不允许出现重复的键(key)
    • Map接口分为两类:TreeMap跟HashMap
      • TreeMap保存了对象的排列次序,hashMap不能
      • HashMap可以有空的键值对(null-null),是非线程安全的,但是可以调用collections的静态方法synchronizeMap()实现
    • HashMap中使用键对象来计算hashcode值
    • HashMap比较快,因为是使用唯一的键来获取对象
  2. 源码解析:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            HashMap.Node<K, V>[] tab;
            HashMap.Node<K, V> p;
            int n, i;
            if ((tab = table) == null || (n = tab.length) == 0) //列表为空或者长度为0 初始化一个HashMap
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)  //当前位置上没有数据new一个链表
                tab[i] = newNode(hash, key, value, null);
            else { //当前数组Hash位置上面已经存在了值
                HashMap.Node<K, V> e;
                K k;
                if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k)))) //有相同的hash键,直接替换
                    e = p;
                else if (p instanceof HashMap.TreeNode) //如果当前是红黑树使用红黑树的添加
                    e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                else { //当前为链表结构的添加
                    for (int binCount = 0; ; ++binCount) { //计算当前链表的长度
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null); //放到链表尾部
                            /**
                             * 当前链表从0开始遍历一直到 7,所以长度大于等于 8转化为红黑树存储,如果remove时树的数据量=<6 ,红黑树会转化成链表结构存储
                             *
                             * 解析:因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。
                             * 链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
                             * 选择6和8,中间有个差值7可以有效防止链表和树频繁转换,而导致的不停转化树与链表导致效率低下的问题;
                             */
                            if (binCount >= TREEIFY_THRESHOLD - 1)
                                treeifyBin(tab, hash); //如果
                            break;
                        }
                        if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount; //记录变化次数,防止在多线程中迭代器取值导致错误,fail-fill检查机制
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    
    1. 通过源码我们可以得出:

      1. 当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到元素在数组中的下标,(hash值计算 = 32位hash的 hash值 ^ 高16位后再 & 上 length -1 )

        1. 如果数组该位置已经存放有其他元素,则在这个位置上以链表的形式存放,新加入的放在链头,最后加入的放在链尾,如果链表中有相应的key则替换value值为最新的并返回旧值;
        2. 如果该位置上没有其他元素,就直接将该位置放在此数组中的该位置上;
        3. 我们希望HashMap里面的元素位置尽量的分布均匀写,使得每个位置上的元素只有一个,这样当使用hash算法得到这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用再遍历链表,这样就大大优化了查询效率;
        4. 计算hash值的方法hash(int h),理论上是对数组长度取模运算 % ,但是消耗比较大,源代码调用为:
         /**
              * Returns index for hash code h.
              */
         static int indexFor(int h, int length) {  
             return h & (length-1); //HashMap底层数组长度总是2的n次方,每次扩容为2倍
         }
         
         //Map数组初始化取值:
         //当 length 总是 2 的 n 次方时,h& (length-1)运算等价于对 length 取模,也就是 h%length,但是 & 比 % 具有更高的效率。
         // Find a power of 2 >= initialCapacity
         int capacity = 1;
             while (capacity < initialCapacity)  
                 capacity <<= 1; //每次增加2倍,所以总长度一定为2的次方
        
        1. 为何hash码初始化为2的次方数探讨:

          image

          分析:

          1. 当它们和 15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8 和 9 会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为 15 的时候,hash 值会与 15-1(1110)进行“与”,那么最后一位永远是 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

          2. 而当数组长度为16时,即为2的n次方时,2n-1 得到的二进制数的每个位上的值都为 1,这使得在低位上&时,得到的和原 hash 的低位相同,加之 hash(int h)方法对 key 的 hashCode 的进一步优化,加入了高位计算,就使得只有相同的 hash 值的两个值才会被放到数组中的同一个位置上形成链表。

          3. 当数组长度为 2 的 n 次幂的时候,不同的 key 算得得 index 相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了

    • 总结: 根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。

      1. HashMap的扩容机制

        1. 什么时候扩容: 达到阈值,数组大小 * 加载因子默认16* 0.75 = 12个
        2. JDK 1.7没有引入红黑树,使用数组加上链表,JDK1.8引入红黑树,使用数组+链表+红黑树存储,当链表值大于等于8转化为红黑树,当remove时红黑树大小小于等于6时会转化为链表,设置一个过渡值7防止不停地put,remove导致不听转化而使得效率低下;
        if (loHead != null) {
                        if (lc <= UNTREEIFY_THRESHOLD) //6
                            tab[index] = loHead.untreeify(map); //将红黑树转化成链表
                        else {
                            tab[index] = loHead;
                            if (hiHead != null) // (else is already treeified)
                                loHead.treeify(tab);
                        }
                    }
        

    归纳HashMap

    1. 简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 hash 算法来决定其在数组中的存储位置,在根据 equals 方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry 时,也会根据 hash 算法找到其在数组中的存储位置,再根据 equals 方法从该位置上的链表中取出该Entry。

    2. 初始化HashMap默认值为16, 初始化时可以指定initial capacity,若不是2的次方,HashMap将选取第一个大于initial capacity 的2n次方值作为其初始长度 ;

    3. 初始化负载因子为0.75,如果超过16 * 0.75 = 12个数据就会将数组扩容为2倍 长度为32 , 并后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。 如果负载印在越大,对空间利用越充分,但是会降低查询效率,如果负载因子越小,则散列表过于稀疏,对空间造成浪费(时间<-> 空间转换: 0.75最佳)

    4. 多线程中的检测机制:Fail-Fast,通过标记modCount(使用volatile修饰,保证线程间修改的可见性) 域 修改次数,在迭代初始化时候赋值,以后每次next的时候都会判断是否相等

      HashIterator() {
          expectedModCount = modCount;
          if (size > 0) { // advance to first entry
          Entry[] t = table;
          while (index < t.length && (next = t[index++]) == null)  
              ;
          }
      }
      final Entry<K,V> nextEntry() {
          if (modCount != expectedModCount)
              throw new ConcurrentModificationException();
      
    5. 建议使用concurrent HashMap在多线程中

    6. Map的遍历,

      1. 使用entrySet获取键值对的Entry集合,只需要遍历一次

      2. 使用keySet先遍历所有的键,在根据键调取get(key),需要遍历两次

      3. JDK1,8以上新增forEach()遍历,先遍历hash表,如果是链表结构遍历后再遍历下一个hash值的链表

      public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
                     Node<K,V>[] tab;
                     if (action == null)
                         throw new NullPointerException();
                     if (size > 0 && (tab = table) != null) {
                         int mc = modCount;
                         // Android-changed: Detect changes to modCount early.
                         for (int i = 0; (i < tab.length && modCount == mc); ++i) {
                             for (Node<K,V> e = tab[i]; e != null; e = e.next)
                                 action.accept(e);
                         }
                         if (modCount != mc)
                             throw new ConcurrentModificationException();
                     }
           }
            Map map = new HashMap();
      

    Iterator iter = map.entrySet().iterator();
      while (iter.hasNext()) {
      Map.Entry entry = (Map.Entry) iter.next();
      Object key = entry.getKey();
      Object val = entry.getValue();
      }

HashMap头添加扩容导致死循环分析

void transfer(Entry[] newTable) {
    Entry[] src = table; //旧的hash表
    int newCapacity = newTable.length;
    //下面这段代码的意思是:
    //  从OldTable里摘一个元素出来,然后放到NewTable中
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next; //注意添加顺序,是由头部开始添加,这个地方会导致循环的产生,加入线程A执行完毕挂起了,此时e = 3 , next = 7 , 3.next = 7的
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

Hashtable(默认容量 11 ,加载因子0.75)

Hashtable与HashMap的比较

  1. HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);

    1. 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它 ;
    2. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
    3. 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小。
    4. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
HashMap jdk1.8红黑树
红黑树
查找 时间复杂度O(logN)
插入
删除

标签:index,hash,HashMap,区别,int,ArrayMap,key,null
来源: https://blog.csdn.net/qq_34368542/article/details/117694759