HashMap,ArrayMap,SparseArray 源码角度分析,Android中的数据结构你该如何去选择?
table = newTab;
可以看到當(dāng)我們的table數(shù)組存儲(chǔ)的節(jié)點(diǎn)值大于threshold時(shí),會(huì)按我們的當(dāng)前數(shù)組大小的兩倍生成一個(gè)新的數(shù)組,并把舊數(shù)組上的數(shù)據(jù)復(fù)制到新數(shù)組上這就是我們的HashMap擴(kuò)容。伴隨著一個(gè)新數(shù)組的生成和數(shù)組數(shù)據(jù)的copy,會(huì)有一定性能上的損耗。如果我們?cè)谑褂肏ashMap的是能夠明確HashMap能夠一開(kāi)始就清楚的知道HashMap存儲(chǔ)的鍵值對(duì)個(gè)數(shù),我建議我們使用HashMap的另一個(gè)構(gòu)造方法。
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
注意了這個(gè)initialCapacity值最好去2的整數(shù)次冪。如果我們要存放40個(gè)鍵值對(duì),那我們這個(gè)initialCapacity最好傳64。至于為什么這樣,我們下次在去討論。
下面我們來(lái)分析HashMap的get方法:
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的時(shí)候,我們首先會(huì)根據(jù)我們的key去計(jì)算它的hash值,如果這個(gè)hash值不存在,我們直接反回null。
如果存在,在沒(méi)有發(fā)生hash沖突的情況下也就是根據(jù)當(dāng)前hash值計(jì)算出的索引上的存儲(chǔ)數(shù)據(jù)不是以樹(shù)和鏈表的形式存儲(chǔ)的時(shí)候,我們直接返回當(dāng)前索引上存儲(chǔ)的值,如果時(shí)鏈表樹(shù),我們就去遍歷節(jié)點(diǎn)上的數(shù)據(jù)通過(guò)equals去比對(duì),找到我們需要的在返回。
通過(guò)上面我可以得出結(jié)論,當(dāng)HashMap沒(méi)有發(fā)生hash沖突時(shí),hashMap的查找和插入的時(shí)間復(fù)雜度都是O(1),效率時(shí)非常高的。
當(dāng)我們發(fā)生擴(kuò)容和hash沖突時(shí),會(huì)帶來(lái)一定性能上的損耗。
HashMap大致分析完了。
下面我們來(lái)分析分析Android為我們提供的ArrayMap和SparseArray。
二.我們?cè)趤?lái)看看ArrayMap:
public class A
rrayMap<K, V> extends SimpleArrayMap<K, V> implements Map<K, V> {
MapCollections<K, V> mCollections;
int[] mHashes;
Object[] mArray;
通過(guò)源碼我們可以看到ArrayMap繼承自SimpleArrayMap實(shí)現(xiàn)了Map接口,ArrayMap內(nèi)部是兩個(gè)數(shù)組,一個(gè)存放hash值,一個(gè)存放Obeject對(duì)象也就是value值,這一點(diǎn)就和HashMap不一樣了。我們現(xiàn)來(lái)看看ArrayMap的構(gòu)造方法:
public ArrayMap(int capacity) {
super(capacity);
}
public SimpleArrayMap() {
mHashes = ContainerHelpers.EMPTY_INTS;
mArray = ContainerHelpers.EMPTY_OBJECTS;
mSize = 0;
}
我們發(fā)現(xiàn)ArrayMap的初始化會(huì)給我們初始化兩個(gè)空數(shù)組,并不像HashMap一樣為我們默認(rèn)初始化了一個(gè)大小為16的table數(shù)組,下面我們繼續(xù)往下看:
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))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方法的實(shí)現(xiàn)。首先就是判段key是否null,是null,hash值直接置為0,如果不為null,通過(guò)Obejct的hashCode()方法計(jì)算出hash值。然后通過(guò)indexfOf方法計(jì)算出index的值。下面我們來(lái)看看indexOf方法:
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方法內(nèi)部是根據(jù)binarySearchHashes()去搜索hash值得,下面我們?cè)賮?lái)看看binarySearchHashes()
內(nèi)部調(diào)用了ContainerHelpers.binarySearch(hashes, N, hash);我們?cè)诳磥?lái)看看binarySearch方法。
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
}
可以發(fā)現(xiàn)binarySearch是典型得二叉搜索算法。所以我們可以得出結(jié)論,ArrayMap插入和索引是基于二叉搜索實(shí)現(xiàn)得。這種搜索得效率也很高,他的時(shí)間復(fù)雜度O(log(n)),但是和HashMap O(1)還是有點(diǎn)差距的。
下面我們繼續(xù)看indexOf方法,如果我們通過(guò)二叉搜索查到得index值小于0,代表我們沒(méi)有存儲(chǔ)過(guò)該數(shù)據(jù)則直接返回,如果index大于0,我們就去通過(guò)equals去比對(duì)原來(lái)索引得上得key,如果相等,代表我們存儲(chǔ)過(guò)該值,直接返回index,到時(shí)候我們存儲(chǔ)的時(shí)候會(huì)直接覆蓋掉當(dāng)前已經(jīng)存儲(chǔ)得值。如果不相等,出現(xiàn)Hash沖突,重新計(jì)算出一個(gè)index值返回。
下面我們來(lái)看看ArrayMap如何處理Hash沖突和擴(kuò)容的(我們沒(méi)有指定容量的時(shí)候,ArrayMap默認(rèn)初始化了兩個(gè)空數(shù)組)。
if (osize >= mHashes.length)
出現(xiàn)hash沖突后,如果我們的存儲(chǔ)數(shù)據(jù)數(shù)量大小已經(jīng)大于等于我們的hash數(shù)組的大小。我們對(duì)數(shù)組進(jìn)行擴(kuò)容。
final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))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(已經(jīng)存儲(chǔ)的多少value個(gè)數(shù))大于等于兩倍的BASE_SIZE(常量為4)我們就在原來(lái)osize的基礎(chǔ)上擴(kuò)容0.5倍,
如果我們的osize小于8(兩個(gè)BASE_SIZE)并且大于4(一個(gè)BASE_SIZE),我們將數(shù)組擴(kuò)容到8,否則我們將數(shù)組大小擴(kuò)容到4。
根據(jù)上面的分析,我們可以得出結(jié)論,ArrayMap的插入和索引是基于二分法的。查找和索引效率不如HashMap。但是要比HashMap占用更少的內(nèi)存空間,HashMap擴(kuò)容實(shí)是在原來(lái)起table的基礎(chǔ)上擴(kuò)容一倍,而ArrayMap實(shí)在存儲(chǔ)數(shù)據(jù)的個(gè)數(shù)上擴(kuò)容0.5倍,不會(huì)造成太多的空間浪費(fèi)。在移動(dòng)設(shè)備上內(nèi)存遠(yuǎn)比PC設(shè)備上值錢(qián)的多。ArrayMap的設(shè)計(jì)就是用時(shí)間換取空間。
三.SparseArray:
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();
Capacity == 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();
總結(jié)
以上是生活随笔為你收集整理的HashMap,ArrayMap,SparseArray 源码角度分析,Android中的数据结构你该如何去选择?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 稻盛和夫的人生法则,所谓人生赢家,不过是
- 下一篇: 自由Android安全研究员陈愉鑫:移动