Android基础学习、HashMap

学习一个新的数据结构,我们需要从这个数据结构的使用入手,比如,我们学习 HashMap,我们就看看 HashMap 是怎么使用的,我们使用 HashMap 最多的方法就是 put 方法。
备注:我们用 Android10.0(API 29) 的源码进行分析

HashMap 的插入流程

我们使用 HashMap 的一般代码

Map<String, String> hashMap = new HashMap<>();
hashMap.put(key, value);

构造函数分析

我们通过构造函数创建了一个HashMap的对象,Map<String, String> hashMap = new HashMap<>(); 那么构造函数做了啥?

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

构造函数给我们创建了一个空的 HashMap 对象,并且指定了初始容量为 16 ,和扩容的因子为 0.75。

问题1、初始容量为啥是16?

为了插入元素时,尽可能的分散,降低 hash 碰撞,其实可以是 2 的任意次幂 ,16 只是一个比较合适的值,我们也可以使用HashMap(int initialCapacity) 构造函数进行初始容量的指定,但是最终的容量一定是 2 的 n 次方(即使我们指定的是一个任意的整数),因为 HashMap 中是这样对容量进行赋值的 this.threshold = tableSizeFor(initialCapacity); , tableSizeFor方法的源码如下:

   /**
    * Returns a power of two size for the given target capacity.
    */
   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;
   }

问题1扩展、 HashMap 的使用优化,指定集合的初始值大小

指定 HashMap 的初始值大小可以避免或减少因为扩容带来的性能损耗和空间损失,比如我们需要存储 7 个元素,那么我们应该怎么写? new HashMap(size = 7) ? 实际 size 应该是 7/0.75 + 1,因为扩容因子 是 0.75,触发扩容的判断是 ++size > threshold。所以我们指定的初始值大小应该为 (数据长度 / 扩容因子)+1


问题2、扩容的因子为什么是 0.75,扩容因子是用来做什么的?

扩容因子的作用,比如当前 HashMap 的容量是 16,当我们存储到 12 个键值对的时候,我们就需要扩大 HashMap的容量,做法是直接乘以2(oldThr << 1 ),上限是 Integer.MAX_VALUE(2^31-1) ,我们看下源码 ,HashMap 定义的最大容量 static final int MAXIMUM_CAPACITY = 1 << 30;(即2^30),当 HashMap 达到规定的最大容量再进行扩容的话,是直接赋值 Integer.MAX_VALUE 的。
当然,我们在正常使用的时候一般都达不到最大值,所以 HashMap 是必然存在空间浪费的,因为它只能存储到最大容量的 0.75 (扩容因子)。至于为啥是 0.75(当然我们也可以设置其他值),我个人的认为是0.75是比较合理的,因为过小容易造成更大的空间浪费,过大的话会造成查询效率降低。

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 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
        }

扩容之后需要对现有的元素重新进行hash运算,然后插入链表,也是非常耗时间的。


HashMap 是怎么插入的

我们存入的时候调用了 put 方法,hashMap.put(key, value); 源码如下

    public V put(K key, V value) {
    		//对key进行hash运算,hash函数也称为散列函数
        return putVal(hash(key), key, value, false, true);
    }

我们看到它不是直接存的,而是对key进行了hash运算,然后 传入 putVal方法。那我们先看什么是hash函数

hash运算

hash函数也称为散列函数,进行hash 运算的目的是为了让结果更均匀的分布,减少哈希碰撞,提升hashmap的运行效率

我们来看下hash函数是怎么计算的

    static final int hash(Object key) {
        int h;
        //无符号右移16位后做异或运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

那么为什么需要这样计算?或者这样计算的好处是什么??
因为这样可以将 hash 值的高低位特征混合起来,
在后面的 hashmap table数组下标的计算中,可以让结果更均匀的分布,减少哈希碰撞,以提升hashmap的运行效率。
(table数组下标计算公式:(n - 1) & hash, 等同于 hash % (n - 1),也就是保证算出来的下标在 [0,n-1] 之间,且会均匀分布, n为数组长度)

这里的具体验证,可以看这篇Blog的分析,沉默的背影: HashMap中的hash算法中的几个疑问

put 方法的源码及逻辑分析

在这里插入图片描述
在这里插入图片描述

红黑树

红黑树是一个高性能的平衡二叉查找树,在插入和删除操作的时候通过特定的操作保持二叉树的平衡,从而获得较高的查找性能。时间复杂度是 O(log n)

红黑树通过变色 和旋转实现自平衡,以保持自身的高查找性能。

关于红黑树的原理和更深的学习,可以看这里红黑树或者自行百度。

什么时候增加的红黑树?

我们翻看Android的源码发现,HashMap 在Android 8.0(API 26,对应的 JDK 版本为 1.8)时增加了 红黑树,在 API 26 (JDK 1.8)之前,HashMap 还只是使用数组 和 链表来实现数据的存储和查找。

这也是 Android 不同版本之间差异的体现,或者不同 JDK 版本之间的差异体现。

Android API 25 时 HashMap 的 put 方法

    //Android API 25 (JDK 1.7)
    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
        int i = indexFor(hash, table.length);
        for (HashMapEntry<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;
    }

HashMap 的查询 get

我们猜想,get 方法 和 put 方法找数组的角标的算法肯定是一样的(不一样的话就没办法通过key取出对应的Value了嘛)
我们来看源码

    public V get(Object key) {
        Node<K,V> e;
        //第一步,是不是一样的对key进行hash运算,然后调用了 getNode 方法来取值,我们接着看 getNode 方法
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //这个判断是什么意思?将table数组赋值给tab临时变量,且数组的长度大于0,
        //并且看到(n - 1) & hash,熟不熟悉,是不是就是上面put方法中计算数组下标的方式,
        //然后找到tab数组对应下标的元素,也就是第一个结点,如果结点不为空继续找,为空直接返回null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //如果hash冲突,且key相同,那就是第一个结点,直接返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //如果第一个结点有后续结点,继续找,否则返回null
            if ((e = first.next) != null) {
                //如果第一个结点是红黑树,则通过getTreeNode去红黑树中查
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //否则,循环链表,直到找到对应元素
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

HashMap 的删除 remove

remove 的删除操作分两边,第一步查,既找到目标结点和get方法逻辑一样,第二步删除,删除只是将结点的引用给去掉就好了,就像上图示意的一样。


小结,顺序表/数组、链表、红黑树

通过上面HashMap的增删查操作,我们大概了解了HashMap的实现原理,HashMap 结合了数组的查询快,链表的插入删除快的优点,提高了我们对数据进行增删查的效率,但是随着链表结点的增多,造成了查询效率的下降,后面又在Android 8.0(API 26)引进了红黑树这个高查找性能的数据结构。
那么这三种数据结构又为什么会有这样的特性呢?

顺序表/数组,ArrayList/Array

ArrayList 为啥插入删除慢,而查询快呢?
其实插删查是不是就对应着ArrayList的三个方法,add,remove,get。那我们来看看源码为什么插入比较慢

  • ArrayList,默认容量 10,private static final int DEFAULT_CAPACITY = 10;
  • ArrayList,最大容量 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;-8是因为一些虚拟机的限制,虚拟机需要保存一些头信息,如果尝试超过这个容量,在某些虚拟机上会报 OutOfMemoryError
  • ArrayList的扩容,每次增加1.5倍,line269:int newCapacity = oldCapacity + (oldCapacity >> 1);,扩容的时候调用Arrays.copyOf()方法,实际是System.arraycopy()方法进行数据移动,System.arraycopy()实际是通过for循环将旧数组的数据赋值给新的数组。
插入数据,add()
    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
    	//数组扩容检查
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //新增的元素插入到size++位置
        elementData[size++] = e;
        return true;
    }

    /**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
    	//角标越界检查
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
		//数组扩容检查
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //通过System.arraycopy进行数组中数据的移动(其实就是用的for循环)
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
		//新增的元素插入到size++位置
        elementData[index] = element;
        size++;
    }

可以看到 ArrayList 插入一个元素的时候,需要进行扩容检查(如果扩容,就需要用arraycopy,for循环移动数据),如果要插入直到指定位置的话,又需要进行一次 arraycopy 既for循环移动数据,所以它的插入效率是比较低的。

查询数据,get()
    /**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
    	//角标越界判断
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        return (E) elementData[index];
    }

我们可以看到,查询数据的时候非常简单,直接通过index角标定位到元素,所以它的查询效率是非常高的

删除数据,remove()
    /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
    	//角标越界判断
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) elementData[index];
		//将删除元素后面所有的元素通过arraycopy向前移动一位
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
		//数组末尾赋值为null,方便GC回收内存
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

同样的删除时的remove方法也需要使用arraycopy方法,for循环移动数据,所以它的删除效率也是比较低的。


链表,LinkedList

链表的特性是查询慢,插入和删除快,那为啥呢?一样的我们来看源码,首先 LinkedList 类中有三个局部变量,分别是链表的首尾结点和结点数量。

    //transient 关键字,表明这个属性不需要被序列化
    transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

LinkedList 是一个双向链表,每一个结点既有上一个结点的引用,也有下一个结点的引用。

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
插入数据,add()
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

    /**
     * Links e as last element.
     */
    void linkLast(E e) {
    	//将尾结点赋值给临时变量l
        final Node<E> l = last;
		//将插入的元素包装成一个新的结点
        final Node<E> newNode = new Node<>(l, e, null);
		//把新结点赋值给尾结点的引用last
        last = newNode;
		//如果之前尾结点为空的话,那么也需要把新结点赋值给头结点first
        if (l == null)
            first = newNode;
        else
			//否则,将新结点接入原尾结点的后面
            l.next = newNode;
        size++;
        modCount++;
    }

我们可以看到,链表的插入只是对内存地址的引用进行修改,所以链表的插入是非常快的。

查询数据,get()

同样的我们来看源码,查询的时候,时间复杂度为O(n),是对链表中元素进行for循环进行查找的,所以效率会比数组/顺序表要低。

时间复杂度 O(n),n表示操作的元素个数,在这里就是size>>1(既size/2),
O(1): 表示算法的运行时间为常量
O(n): 表示该算法是线性算法
O(㏒2n): 二分查找算法

    /**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
    	//检查角标越界
        checkElementIndex(index)
        //找到链表中的目标结点
        return node(index).item;
    }

    /**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(index);
		//通过index进行二分,然后在前半区/后半区for循环查找
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
删除数据,remove()

删除的操作,第一步是查,第二步是删除。我们看源码可以发现,第一步查找的代码和get方法是一样的,优化的或者效率高的在第二步删除的操作,也就是unlink方法中,通过unlink的源码可以看到,删除操作就是将被删结点的前一个结点直接指向被删结点的下一个结点,并把被删结点的值赋值为null,方便后面GC进行内存回收。

    /**
     * Removes the element at the specified position in this list.  Shifts any
     * subsequent elements to the left (subtracts one from their indices).
     * Returns the element that was removed from the list.
     *
     * @param index the index of the element to be removed
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
    	//检查角标越界
        checkElementIndex(index);
		//先调用node(index)找到对应的结点,然后调用unlink方法将结点删除
        return unlink(node(index));
    }

    /**
     * Unlinks non-null node x.
     */
    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

HashMap 的线程安全(HashTable)

虽然HashMap是我们最经常使用的数据结构,但是如果我们在多线程中使用HashMap会出现线程安全问题,比如线程1正在查询目标结点的时候,线程2正在删除目标结点。会出现什么情况?轻则执行失败,重则出现异常闪退。

那怎么解决HashMap的线程安全问题呢?答案是HashTable。我们来看下HashTable的插入操作。

  • HashTable的put方法:
    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        HashtableEntry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        HashtableEntry<K,V> entry = (HashtableEntry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

可以看到和上面 HashMap 的 put 方法的区别在哪里?区别在于 HashTable 给put 方法加上了 synchronized 同步锁。
锁定的是 HashTable 的当前对象,所以在进行put操作的时候,就不能进行get / remove 操作。
**所以HashTable虽然解决了 HashMap 的线程安全问题,但是也造成了性能的大幅下降,因为同时只能操作一个方法,那么还能不能优化HashTable的效率呢?答案是能,我们接着看 ConcurrentHashMap **

synchronized,是一个内置锁,开锁 和 关锁 都是由 JVM 完成。


ConcurrentHashMap解决HashMap的线程安全

我们来看下 ConcurrentHashMap 的 put 方法的源码,我们可以看ConcurrentHashMap是加锁了,但是没有像HashTable那样给所有方法都加上锁,而是给正在操作的链表加上了锁。这样既解决了HashMap的线程安全的问题,也兼顾了性能的处理。

    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

HashMap,HashTable,ConcurrentHashMap的形象示意


HashMap和SparseArray

性能对比,内存、时间

当我们用HashMap存储以 int/Integer为key的键值对的时候,被提示使用SparseArray。那么为啥呢?
我们做一个性能对比,这里我引用了网上的一个实验

分别用HashMap和SparseArray存储10000个键值对,key就用for循环的角标 i,范围在[0,9999],value就存一个10byte的空字节数组。
发现 HashMap 大概用了0.7M的内存空间,SparseArray 大概用了0.5M的内存空间。
然后,我们分别用HashMap和SparseArray来get10000次,看下消耗的时间,
发现 HashMap 用了509毫秒,SparseArray 用了10毫秒。

那么为什么有这么大的差别呢?

HashMap 耗时的原因

1、新插入的结点,需要轮询链表上所有的结点,看是否已经插入

2、HashMap 扩容(默认容量达到0.75时需要进行扩容),扩容的过程是一个rehash的过程 ,既对现有的元素重新进行hash运算,然后插入链表,也是非常耗时间的。

HashMap 耗空间的原因

1、SparseArray的key存储的int 类型的,一个key只占4个字节,但是 HashMap 的key 是 Object类型,这个就比 SparseArray 更占空间,
而且SparseArray 只用存储 key 和 value 的值,但是SparseArray 不仅需要存储key,Value还需要存储key对应的hash值,以及上下两个结点的引用。

2、HashMap有扩容因子,预值是0.75 ,当达到容量的0.75时就需要扩容,所以 HashMap 存在空间浪费。
空间浪费是为了减少hash冲突,如果不浪费空间,最极端的情况就是所有结点存在一个链表上,HashMap 就变成链表了,查找效率就变低了。

SparseArray

SparseArray 的形象示意

SparseArray ,使用了两个数组来存储数据,用整形数组存储key,用object数组存储value。

private int[] mKeys;
private Object[] mValues;
插入数据 put
    /**
     * Adds a mapping from the specified key to the specified value,
     * replacing the previous mapping from the specified key if there
     * was one.
     */
    public void put(int key, E value) {
    	//二分查找,找到key在数组中将要插入位置的角标
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
		//大于0,说明key之前就存在,直接对mValues数组对应位置进行赋值
        if (i >= 0) {
            mValues[i] = value;
        } else {
        //i为负数,说明插入的key之前不存在,对i取反,就是将要插入的位置
            i = ~i;
			//如果插入位置没有越界,且这个位置是已删除的标记,复用这个位置
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
			//如果需要GC 和 扩容
            if (mGarbage && mSize >= mKeys.length) {
				//触发GC
                gc();
				//GC后,插入的下标i,可能发生变化,重新查找插入位置
                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
			//可自增的数组工具类插入,用的的System.arraycopy
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
    }

我们通过源码可以看出
1、SparseArray 是用两个数组存储键值对的,两个数组的角标是严格一 一对应的
2、删除数据时,不是直接删除而是打上删除标记,也就是给value赋值为DELETED对象
3、查找键值对的时候是通过对key的二分查找实现的
4、数组的扩容规则是,如果当前数组的容量<=4,并且达到扩容的条件(currentSize + 1 > array.length),直接扩容为8,否则直接*2

    public static int growSize(int currentSize) {
        return currentSize <= 4 ? 8 : currentSize * 2;
    }
查询数据 get
    /**
     * Gets the Object mapped from the specified key, or <code>null</code>
     * if no such mapping has been made.
     */
    public E get(int key) {
        return get(key, null);
    }

    /**
     * Gets the Object mapped from the specified key, or the specified Object
     * if no such mapping has been made.
     */
    @SuppressWarnings("unchecked")
    public E get(int key, E valueIfKeyNotFound) {
    	//同样的对key进行二分查找
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
		//如果角标小于0(没找到),或者对应元素是被删除的,告诉外面没找到
        if (i < 0 || mValues[i] == DELETED) {
            return valueIfKeyNotFound;
        } else {
        	//否则取value数组中对应角标的值返回
            return (E) mValues[i];
        }
    }
删除数据 remove/delete
    /**
     * Alias for {@link #delete(int)}.
     */
    public void remove(int key) {
        delete(key);
    }

    /**
     * Removes the mapping from the specified key, if there was any.
     */
    public void delete(int key) {
    	//同样的二分查找
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
		//找到了,如果对应值不是删除状态,赋值为DELETED(空对象)
        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
				//垃圾回收状态置为true,后面调用GC用到
                mGarbage = true;
            }
        }
    }

这里我们也可以看到一般的删除都是分两个步骤,1查找,2删除,
这里SparseArray的删除做法是啥,不是真删,而是用一个空对象DELETED,来标记,表明这里是删除的。这样做的好处是,当我们插入、删除时不需要频繁的调用
System.arrayCopy方法,前面我们看过了System.arrayCopy就是new一个新的数组,然后将旧数据的值循环赋值到新数组 ,这样是很耗性能的,而这种
通过删除标记处理删除的方式,则可以让SparseArray在大量删除、插入操作的时候保持高性能,这个思想也是我们日常开发中可以用到的优化思想。


再次小结,生产环境下的优化

1、在生产环境中,如何选择数组或者链表进行数据存储?

看使用场景,如果一次插入,后续查询操作较多,应该使用数组;如果查询需求较小,有大量的插入删除操作可以选择链表。
那如果我查询和插入删除都很多该怎么选呢?那么我们可以选择数组,但是删除的时候不用真正删除,而是利用删除标记进行优化。

2、在生产环境中,如何优化数组大量插入删除操作的效率?

利用空间换时间,可以对要删除的位置进行删除标记,比如存入null或者空对象,等内存不够或者合适的时机再进行GC,回收内存。

3、HashMap为什么存储键值对,而不是直接存储value?

key 用于快速的查找数组,
key ,是一个 Object 的对象,通过 key.hashCode() 函数可以得到一个int 类型的hash值,然后和 HashMap 数组的长度-1 进行 & 运算,可以得到[0,数组长度-1]之间的值,这个就是数组的角标,通过这个角标就可以快速的找到对应的链表,进行存储或查询。

不知道整理的对不对,如果有错漏或不足的地方,烦请指正。

20210410,订正的时候发现有两张截图从本地笔记本拷贝到CSDN的时候没有上传成功,下次注意!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值