android每日面试题7之ArrayMap、HashMap、SparseArray 原理

我知道的:

 这三个map用的最多的是就是HashMap,其他的用的比较少。HashMap是无序的,key-value键值对。key可以为空。

他的key是通过hash运算存到里面的。采用数组加链表的方式来存储,在下面就是红黑树。 为什么采用数组加链表,因为hash冲

突在一个个butlet桶下面形成链表。寻找下面的键值。

一.ArrayMap

int[] mHashes; // 存储出的是每个key的hash值,并且在这些key的hash值在数组当中是从小到大排序的。
Object[] mArray; // 长度是mHashs的两倍,每两个元素分别是key和value,这两元素对应mHashs中的hash值。

看到他的构造方法最终调用ArrayMap(int capacity, boolean identityHashCode)这个方法。传入的两个参数:capaacity表示初始的容量,identityHashCode这个参数为false他表示计算hashcode的方式由System调用还是由Object调用。

 @Override
    public V put(K key, V value) {
        //当前数组的长度
        final int osize = mSize;
        final int hash;
        int index;
        //判断key是否为空 为空的话hash赋值为0,寻找为空的下标。
        if (key == null) {
            hash = 0;
            index = indexOfNull();
        } else {
            //不为空的话选择hashcode的计算方式,一般为false 也就是key.hashCode就是自己的hash方法
            hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
            //寻找hash在数组的下标
            index = indexOf(key, hash);
        }
        //判断下标是否大于0,如果大于表示这个值在已经存在过了。然后把当前下标位置替换新的值。
        if (index >= 0) {
            index = (index << 1) + 1;
            final V old = (V) mArray[index];
            mArray[index] = value; //新的value值进行替换
            return old;
        }

        index = ~index;
        //判断当前长度是否大于保存hash值数组的长度,进行数组扩容
        if (osize >= mHashes.length) {
            //BaseSize是4
            //如果当前长度大于8 则增长2倍
            //否则容量大于4,则扩容到8.
            //否则扩容到4
            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);

            final int[] ohashes = mHashes; //临时hash数组
            final Object[] oarray = mArray;//临时保存value的数组
            //扩容到算出大小
            allocArrays(n);

            //判断两个长度是否一样,否则抛出concurrentModification异常
            if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
                throw new ConcurrentModificationException();
            }
            //把临时数组中的值移动到新数组
            if (mHashes.length > 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);
            }
            //释放数组空间,里面的方法看到把数组元素致空
           /* array[0] = mBaseCache;
            array[1] = hashes;
            for (int i = (size << 1) - 1; i >= 2; i--) {
                array[i] = null;
            }*/
            freeArrays(ohashes, oarray, osize);
        }

        //当前位置下标在当前数组中则添加到数组中,其他值往后移动
        if (index < osize) {
            if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize - index)
                    + " to " + (index + 1));
            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();
            }
        }
        //按照存入顺序添加hash 数组
        mHashes[index] = hash;
        //在value数组 index*2下标存入key值 index*2+1的位置存入value值
        mArray[index << 1] = key;
        mArray[(index << 1) + 1] = value;
        mSize++;
        return null;
    }

太长了没有截图全,我把代码贴在上面,主要是put值的操作。主要是

1.判断是否传入的key值为空,hash值为0,寻找为空的下标。不为空的话找到在hash数组中hash的下标。

2.判断下标是否在数组中是否存在过,替换新的值。

3.判断长度是否需要进行扩容, 当前长度大于8则增长2倍,大于4则扩容到8,小于4则扩容到4。创建临时变量,进行数组拷贝

进行扩容。

4.释放数组。

二.SparseArray

private int[] mKeys; //采用int数组存入key值
private Object[] mValues; //object数组存入value值

默认构造器传入默认长度是为10。

寻找当前坐标,当前坐标在数组中存在直接替换。否则添加新的键值对,数组空间够的的直接添加,不够进行扩容机制,然后重新计算下标,在添加。

//GrowingArrayUtils.java
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
        assert currentSize <= array.length;

        if (currentSize + 1 <= array.length) {
            //如果当前数组容量充足,先将当前下标index往后移动
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            //在将要添加的元素放到下标为index的地方
            array[index] = element;
            return array;
        }
        
        //如果容量不足,先进行扩容生成新的数组newArray
        @SuppressWarnings("unchecked")
        T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
                growSize(currentSize));
        //将原数组中index个元素拷贝到新数组中
        System.arraycopy(array, 0, newArray, 0, index);
        //将要添加的元素添加到index位置
        newArray[index] = element;
        //将原数组中index+1之后的元素拷贝到新数组中
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }

    public static int growSize(int currentSize) {
        //扩容计算规则,当前容量小于5返回8;否则返回2倍的容量
        return currentSize <= 4 ? 8 : currentSize * 2;
    }

 数组空间不够的话,是跟arraymap差不多的直接创建一个临时变量,然后把值移动到扩容的数组。扩容机制是当前小于5则返回8,否则返回当前数组长度的二倍。

三.HashMap

创建一个HashMap上面注释说了默认的大小是16,负载因子是0.75

这个太多了,明天早上补上困的不行了。。。

人啊就是不能拖延说今天早上写,到下午了。又来恰鸡了。。

构造方法最终调用的是他,初始化了三个参数initialCapacity,loadFactor,threshold。

initalCapacity初始化容量(默认16):hashmap底层由数组+链表或红黑树实现。一开始是数组,当数据越来越多,需要进行扩容

操作。如果知道自己需要存储数据大小情况下,指定容量。避免扩容可以提升效率的。

threshold 阈值:hashMap所能容纳的最大价值对数量,如果超过则需要扩容,计算方threshold=initialCapacity*loadFactor(构

造方法中直接通过tableSizeFor(initialCapacity)方法进行了赋值

loadFactor 加载因子(默认0.75):当负载因子比较大时,数组的扩容可能行会少,但是每条entry链表就相对比较多,查询时间

,变成。当负载因子比较小时数组的扩容的可能性就会高。链表元素会相对少一些,查找时间会减少。所有负载因子是时间上的

一种折中的说法。

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            //判读数组是否初始化,然后进行初始化容量,阀值
            n = (tab = resize()).length;

        //通过hash值找到下标,如果为空直接赋值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else //通过hash值找到的位置有数据,发生冲突
            {
            Node<K,V> e; K k;
                //如果需要插入的key和当前hash值指定下标的key一样,先将e数组中已有的数据
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //如果此时桶中数据类型为 treeNode,使用红黑树进行插入
            else if (p instanceof TreeNode)
                e = ((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);

                        //如果链表过长,达到树化阈值,将链表转化成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果链表中有新插入的节点位置数据不为空,则此时e 赋值为节点的值,跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
                //经过上面的循环后,如果e不为空,则说明上面插入的值已经存在于当前的hashMap中,那么更新指定位置的键值对
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

put方法分为三种情况:

1.table尚未初始化,对数据进行初始化

2.table已经初始化,且通过hash算法找到下标所在的位置数据为空,直接将数据存放到指定位

3.table已经初始化,且通过hash算法找到下标所在的位置数据不为空,发生hash冲突(碰撞),发生碰撞后,会执行以下操作

  • 判断插入的key如果等于当前位置的key的话,将 e 指向该键值对  
  • 如果此时桶中数据类型为 treeNode,使用红黑树进行插入
  • 如果是链表,则进行循环判断, 如果链表中包含该节点,跳出循环,如果链表中不包含该节点,则把该节点插入到链表末尾,同时,如果链表长度超过树化阈值(TREEIFY_THRESHOLD)且table容量超过最小树化容量(MIN_TREEIFY_CAPACITY),则进行链表转红黑树(由于table容量越小,越容易发生hash冲突,因此在table容量<MIN_TREEIFY_CAPACITY 的时候,如果链表长度>TREEIFY_THRESHOLD,会优先选择扩容,否则会进行链表转红黑树操作)

 

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;

        //1、table已经初始化,且容量 > 0
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                //如果旧的容量已近达到最大值,则不再扩容,阈值直接设置为最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //如果旧的容量不小于默认的初始容量,则进行扩容,容量扩张为原来的二倍
                newThr = oldThr << 1; // double threshold
        }
        //2、阈值大于0 threshold 使用 threshold 变量暂时保存 initialCapacity 参数的值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //3 threshold 和 table 皆未初始化情况,此处即为首次进行初始化
        //也就在此处解释了构造方法中没有对threshold 和 初始容量进行赋值的问题
        else {               // zero initial threshold signifies using defaults
            //如果阈值为零,表示使用默认的初始化值
            //这种情况在调用无参构造的时候会出现,此时使用默认的容量和阈值
            newCap = DEFAULT_INITIAL_CAPACITY;
            //此处阈值即为 threshold=initialCapacity*loadFactor
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // newThr 为 0 时,按阈值计算公式进行计算,容量*负载因子
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }

        //更新阈值
        threshold = newThr;

        //更新数组桶
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;

        //如果之前的数组桶里面已经存在数据,由于table容量发生变化,hash值也会发生变化,需要重新计算下标
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //如果指定下标下有数据
                if ((e = oldTab[j]) != null) {
                    //1、将指定下标数据置空
                    oldTab[j] = null;
                    //2、指定下标只有一个数据
                    if (e.next == null)
                        //直接将数据存放到新计算的hash值下标下
                        newTab[e.hash & (newCap - 1)] = e;
                    //3、如果是TreeNode数据结构
                    else if (e instanceof TreeNode)

                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //4、对于链表,数据结构
                    else { // preserve order
                        //如果是链表,重新计算hash值,根据新的下标重新分组
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
public HashMap(int initialCapacity, float loadFactor) {
    /**
     * 找到大于或等于 cap 的最小2的幂
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

 这段方法是扩容的方法,他的扩容方法是找到 tablesizefor大于他那最下2的幂,然后*0.75负载因子计算的,来判断是否需要扩容。加入初始化容量是1000的话就是 2的9次幂 1024然后乘与负载因子默认0.75 是 768 。如果你要存入1000条数据的话。是会要进行一次扩容的。

resize方法逻辑比较复杂,需要静下心来一步步的分析,但是总的下来,分为以下几步:

首先先判断当前table是否进行过初始化,如果没有进行过初始化,此处就解决了调用无参构造方法时候,threshold和

initialCapacity 未初始化的问题,如果已经初始化过了,则进行扩容,容量为原来的二倍扩容后创建新的table,并对所有的数据

进行遍历

             如果新计算的位置数据为空,则直接插入

            如果新计算的位置为链表,则通过hash算法重新计算下标,对链表进行分组

           如果是红黑树,则需要进行拆分操作

 

SparseArray 稀疏矩阵

  • SparseArray 存储 整型类型的 key
  • SparseArray 比HashMap 更省内存,某些条件下 性能更好,主要是因为它避免了对key的自动装箱。

ArrayMap

  • ArrayMap 是一个 <key,value>映射的数据结构。它设计上更多考虑内存的优化。内部是使用两个数组进行数据存储,一个数组记录key的hash值,另外一个数组记录Value值。
  • 它和SparseArray一样,也会对key使用二分法进行从小到大排序。在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,然后通过index来进行添加、查找、删除等操作。
  • ArrayMap 与 SparseArray最大的一点不同就是 ArrayMap的key可以为任意的类型。而SparseAraay的key只能是整型。

HashMap

  • 包装类型的key和value
  • 计算对象的哈希值
  • 包含下一个Entry的指针

缺点

自动装拆箱的操作会对内存和GC有影响。

HashMapEntry是一层额外的封装

每次扩容时会重新排列(参考HashMap.transfer方法)

Hash算法不佳导致退化成链表

三者的使用场景:

HashMap 与 SparseArray比较

  • 当数据量在1000以上,推荐使用HashMap。
  • 当数据量 在500-1000,HashMap 和SparseArray性能差不多。
  • 当数据量 少于500时,使用SparseArray 要优于HashMap。

SparseArray 与 ArrayMap使用场景:

  • 当 key为整型时,推荐使用SparseArray
  • 当 key为其它类型时,推荐使用ArrayMap
  •  

    •  

    •  

    •  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值