HashMap,ArrayMap,SparseArray 源码角度分析,Android中的数据结构你该如何去选择?
引言:
Map集合,以key-value形式存储的数据结构,是我们在Android开发过程中经常需要用到的。
除了java.util包下,为我们提供的HashMap是我们开发中经常使用的,Android也为我们提供了,两种以key-value形式存储的数据结构,一个是ArrayMap,一个是SparseArray。那么这三种Map集合我在日常的开发过程中该去如何抉择呢。
下面让我来从源代码的角度分析一下,这三种集合的实现方式和原理。
一.首先我们首先来看HashMap:
1.HashMap的put方法:
[code] public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } 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; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; 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; } 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; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
我们来分析一下上面的代码,当我们调用put方法的时候,实际上调用的是putVal()方法。在调用的putVal的时候我们调用了HashMap内部的hash方法来根据我们的key得到一个hash值。
[code] static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
当我第一次调用put方法的时候我们的table数组为null,putVal方法内部,会帮我们调用resize()方法帮我们生成一个默认大小的
数组。默认大小就是我们的DEFAULT_INITIAL_CAPACITY的值,为16。
[code]static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
数组初始化完成之后,会根据通过hash方法生成hash值与(n-1)做&运算计算出我们要存放数据的索引。n就是我们table的大小。
如果当前数组在这个索引没有值,则直接存储我们的value。如果当前数组在这个索引有值,三种情况去处理。
1.if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
如果当前索引存放的值的key的hash值等于我们现在put的key的hash值,并且我们的key和当前索引存放的值是相等的,那我们就覆盖当前索引的值。(HashMap 数据不会重复,是否存放的同一个对象是通过key去判断的)。
2.else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
如果当前储存的值是TreeNode(树节点)。代表出现了hash冲突,我们为当前树新增一个叶子节点存放我们的数据。
3.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; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
否则我们生成一个新的节点,存放在当前值的后面形成一个链表。如果链表的节点数量大于TREEIFY_THRESHOLD - 1
则调用treeifyBin(tab, hash);把当前链表构造成一个树,存放在当前数组的索引上。
分析到这我们可以得出结论,HashMap是使用的链地址法解决的Hash冲突,如果链表的节点个数过多,我们就链表结构转换为
树结构。
我们继续往下看
[code]if (++size > threshold) resize();
如果当前存储存储的值的个数,大于threshold,我们调用resize方法。我们的threshold值等于什么呢 等于我们当前数组大小乘以一个DEFAULT_LOAD_FACTOR(装填因子),通过HashMap源码我们可以看到我们的DEFAULT_LOAD_FACTOR默认为0.75.
所以我们默认threshold为(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY),等于12。
下面我们继续来分析resize()方法。
[code]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 } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab;
可以看到当我们的table数组存储的节点值大于threshold时,会按我们的当前数组大小的两倍生成一个新的数组,并把旧数组上的数据复制到新数组上这就是我们的HashMap扩容。伴随着一个新数组的生成和数组数据的copy,会有一定性能上的损耗。如果我们在使用HashMap的是能够明确HashMap能够一开始就清楚的知道HashMap存储的键值对个数,我建议我们使用HashMap的另一个构造方法。
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
注意了这个initialCapacity值最好去2的整数次幂。如果我们要存放40个键值对,那我们这个initialCapacity最好传64。至于为什么这样,我们下次在去讨论。
下面我们来分析HashMap的get方法:
[code] public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { 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; }
在get的时候,我们首先会根据我们的key去计算它的hash值,如果这个hash值不存在,我们直接反回null。
如果存在,在没有发生hash冲突的情况下也就是根据当前hash值计算出的索引上的存储数据不是以树和链表的形式存储的时候,我们直接返回当前索引上存储的值,如果时链表树,我们就去遍历节点上的数据通过equals去比对,找到我们需要的在返回。
通过上面我可以得出结论,当HashMap没有发生hash冲突时,hashMap的查找和插入的时间复杂度都是O(1),效率时非常高的。
当我们发生扩容和hash冲突时,会带来一定性能上的损耗。
HashMap大致分析完了。
下面我们来分析分析Android为我们提供的ArrayMap和SparseArray。
二.我们在来看看ArrayMap:
[code]public class ArrayMap<K, V> extends SimpleArrayMap<K, V> implements Map<K, V> { MapCollections<K, V> mCollections; int[] mHashes; Object[] mArray;
通过源码我们可以看到ArrayMap继承自SimpleArrayMap实现了Map接口,ArrayMap内部是两个数组,一个存放hash值,一个存放Obeject对象也就是value值,这一点就和HashMap不一样了。我们现来看看ArrayMap的构造方法:
[code] public ArrayMap(int capacity) { super(capacity); } public SimpleArrayMap() { mHashes = ContainerHelpers.EMPTY_INTS; mArray = ContainerHelpers.EMPTY_OBJECTS; mSize = 0; }
我们发现ArrayMap的初始化会给我们初始化两个空数组,并不像HashMap一样为我们默认初始化了一个大小为16的table数组,下面我们继续往下看:
[code]public V put(K key, V value) { final int osize = mSize; final int hash; int index; if (key == null) { hash = 0; index = indexOfNull(); } else { hash = key.hashCode(); index = indexOf(key, hash); } if (index >= 0) { index = (index<<1) + 1; final V old = (V)mArray[index]; mArray[index] = value; return old; } index = ~index; if (osize >= mHashes.length) { 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; final Object[] oarray = mArray; allocArrays(n); 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); } 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(); } } mHashes[index] = hash; mArray[index<<1] = key; mArray[(index<<1)+1] = value; mSize++; return null; }
我们先看看put方法的实现。首先就是判段key是否null,是null,hash值直接置为0,如果不为null,通过Obejct的hashCode()方法计算出hash值。然后通过indexfOf方法计算出index的值。下面我们来看看indexOf方法:
[code] 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; } int index = binarySearchHashes(mHashes, N, hash); // If the hash code wasn't found, then we have no entry for this key. if (index < 0) { return index; } // If the key at the returned index matches, that's what we want. if (key.equals(mArray[index<<1])) { return index; } // Search for a matching key after the index. int end; for (end = index + 1; end < N && mHashes[end] == hash; end++) { if (key.equals(mArray[end << 1])) return end; } // Search for a matching key before the index. for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) { if (key.equals(mArray[i << 1])) return i; } // Key not found -- return negative value indicating where a // new entry for this key should go. We use the end of the // hash chain to reduce the number of array entries that will // need to be copied when inserting. return ~end; }
我们可以看到indexOf方法内部是根据binarySearchHashes()去搜索hash值得,下面我们再来看看binarySearchHashes()
内部调用了ContainerHelpers.binarySearch(hashes, N, hash);我们在看来看看binarySearch方法。
[code] static int binarySearch(int[] array, int size, int value) { int lo = 0; int hi = size - 1; while (lo <= hi) { int mid = (lo + hi) >>> 1; int midVal = array[mid]; if (midVal < value) { lo = mid + 1; } else if (midVal > value) { hi = mid - 1; } else { return mid; // value found } } return ~lo; // value not present }
可以发现binarySearch是典型得二叉搜索算法。所以我们可以得出结论,ArrayMap插入和索引是基于二叉搜索实现得。这种搜索得效率也很高,他的时间复杂度O(log(n)),但是和HashMap O(1)还是有点差距的。
下面我们继续看indexOf方法,如果我们通过二叉搜索查到得index值小于0,代表我们没有存储过该数据则直接返回,如果index大于0,我们就去通过equals去比对原来索引得上得key,如果相等,代表我们存储过该值,直接返回index,到时候我们存储的时候会直接覆盖掉当前已经存储得值。如果不相等,出现Hash冲突,重新计算出一个index值返回。
下面我们来看看ArrayMap如何处理Hash冲突和扩容的(我们没有指定容量的时候,ArrayMap默认初始化了两个空数组)。
[code]if (osize >= mHashes.length)
出现hash冲突后,如果我们的存储数据数量大小已经大于等于我们的hash数组的大小。我们对数组进行扩容。
[code]final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1)) : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
[code]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); }
如果我们的osize(已经存储的多少value个数)大于等于两倍的BASE_SIZE(常量为4)我们就在原来osize的基础上扩容0.5倍,
如果我们的osize小于8(两个BASE_SIZE)并且大于4(一个BASE_SIZE),我们将数组扩容到8,否则我们将数组大小扩容到4。
根据上面的分析,我们可以得出结论,ArrayMap的插入和索引是基于二分法的。查找和索引效率不如HashMap。但是要比HashMap占用更少的内存空间,HashMap扩容实是在原来起table的基础上扩容一倍,而ArrayMap实在存储数据的个数上扩容0.5倍,不会造成太多的空间浪费。在移动设备上内存远比PC设备上值钱的多。ArrayMap的设计就是用时间换取空间。
三.SparseArray:
[code] public SparseArray() { this(10); } public SparseArray(int initialCapacity) { if (initialCapacity == 0) { mKeys = EmptyArray.INT; mValues = EmptyArray.OBJECT; } else { mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity); mKeys = new int[mValues.length]; } mSize = 0; } public void put(int key, E value) { int i = ContainerHelpers.binarySearch(mKeys, mSize, key); if (i >= 0) { mValues[i] = value; } else { i = ~i; if (i < mSize && mValues[i] == DELETED) { mKeys[i] = key; mValues[i] = value; return; } if (mGarbage && mSize >= mKeys.length) { gc(); // Search again because indices may have changed. i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); } mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); mSize++; } }
从SparseArray构造方法我们可以看出,SparseArray只允许我们存储key为int型的数据,在初始化默认会给我们初始化两个大小
都为10数组,一个存放key值,一个存放value(和ArrayMap值)。在分析get方法,我们发现SparseArray和ArrayMap一样,都是
基于二分法来插入和索引的。和ArrayMap不同的时,SparseArray把扩容操作交给了GrowingArrayUtils的insert方法。
[code] public static <T> T[] insert(T[] array, int currentSize, int index, T element) { assert currentSize <= array.length; if (currentSize + 1 <= array.length) { System.arraycopy(array, index, array, index + 1, currentSize - index); array[index] = element; return array; } @SuppressWarnings("unchecked") T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(), growSize(currentSize)); System.arraycopy(array, 0, newArray, 0, index); newArray[index] = element; System.arraycopy(array, index, newArray, index + 1, array.length - index); return newArray; } public static int growSize(int currentSize) { return currentSize <= 4 ? 8 : currentSize * 2; }
可以看到当currentSize(当前存储的值的个数)小于等于4的时候,扩容至8,否则扩容至两倍的currentSize。
好了到这里,HashMap,ArrayMap,SparseArray的源码我们已经分析完了。下面我们来做一个总结。
四.结论
HashMap | ArrayMap | SparseArray | |
初始化 | 初始化默认大小16的table数组 | 初始化两个空数组(一个存放hash值,一个存放value) | 初始化大小为10的两个数组(一个存放key值,一个存放value) |
查找和删除 |
散列表 不发生hash冲突时O(1) |
二分法 O(logN) | 二分法 O(logN) |
插入数据要求 |
任意对象 key值能为null |
任意对象key 值能为null |
key值只能为int |
扩容 |
table的两倍 (最好为2的整数次幂) |
4,8,或者已存储数据个数的1.5倍 | 8,或者已存储数据个数的2倍. |
解决hash冲突 | 链地址法,链表过长转为红黑树 |
出现冲突 根据hash值重新计算出一个index |
不会出现hash冲突。因为key值为int。如果int值重复直接覆盖原始索引上的数据。 |
在Android开发中,如果我们明确了自己要存储的数据的个数,使用这个三个数据结构的时候,最好在初始化的时候,传递一个大小,这样可以有效的避免因为存储数据的增多,导致数组扩容带来的性能上的损耗。
如果我们要存储的数据不多,查找和插入使用二分法效率可以接受得话,我建议我们使用ArrayMap代替HashMap存储,因为HashMap的内存占用要比ArrayMap多的多。在移动设备上内存比那么一点点的性能来重要的多。
如果我们要存储的数据的key值为int,那我们最好使用SparseArray,SparseArray也是基于二分法来插入和索引的,数据量不大的情况下,查找和插入的效率也很高,不要用ArrayMap,HashMap,ArrayMap,HashMap如果要存储int类型的key涉及到拆装箱的操作,会有一定性能损耗。
如果我们要存储的数据量很大,涉及频繁的插入和读取,我建议我们使用HashMap,因为HashMap的查找效率是最高的,在不发生hash冲突的情况下时间复杂度是O(1)。
---------------------------------------------
转载标明原文地址哦:https://blog.csdn.net/braintt/article/details/86659792
- Android探索之旅(第十二篇)HashMap,ArrayMap,SparseArray源码分析及性能对比
- HashMap,ArrayMap,SparseArray源码分析及性能对比
- <转载>Android性能优化之HashMap,ArrayMap和SparseArray
- android SparseArray 源码分析
- android SparseArray替代HashMap的分析
- Android开发中高效的数据结构用SparseArray代替HashMap
- Android内存优化之取代HashMap(SparseArray和ArrayMap解析)
- Android开发中高效的数据结构用SparseArray代替HashMap
- Android SparseArray与HashMap与ArrayMap的性能差别
- Android版数据结构与算法(四):基于哈希表实现HashMap核心源码彻底分析
- Android源码分析(五)-----如何从架构师的角度去设计Framework框架
- Android开发中高效的数据结构用SparseArray代替HashMap
- Android开发中高效的数据结构用SparseArray代替HashMap
- Android开发中高效的数据结构用SparseArray代替HashMap
- Android开发中高效的数据结构用SparseArray代替HashMap
- 从源码角度分析android蓝牙设备如何互联?
- Android开发中高效的数据结构用SparseArray代替HashMap
- android SparseArray替代HashMap的分析
- Android开发中高效的数据结构用SparseArray代替HashMap
- Android开发中高效的数据结构用SparseArray代替HashMap