上一篇文章提到了SparseArray和HashMap的关系,与SparseArray同样用于改进HashMap效率的还有ArrayMap,当然这两个类都是在特定情况下用于改进效率,并不是任何情况下都优于HashMap,下面将会一步步讲解ArrayMap的内部解构以及代码的组成是什么样的
1. 初始化
// 传入初始容量
public ArrayMap(int capacity) {
this(capacity, false);
}
public ArrayMap(int capacity, boolean identityHashCode) {
mIdentityHashCode = identityHashCode;
// 根据传入的初始容量给`mArray` `mHashes`两个数组初始化
if (capacity < 0) {
mHashes = EMPTY_IMMUTABLE_INTS;
mArray = EmptyArray.OBJECT;
} else if (capacity == 0) {
mHashes = EmptyArray.INT;
mArray = EmptyArray.OBJECT;
} else {
allocArrays(capacity);
}
mSize = 0;
}
2. put操作
put相关的代码比较长,涉及到的逻辑也比较多,在这其中也会设计到扩容以及缓存的优化操作
@Override
public V put(K key, V value) {
// 记录下当前长度
final int osize = mSize;
final int hash;
int index;
// 如果传入的key为null,那么hash值默认为0,并且indexOfNull会通过二分查找
// 返回一个下标,至于具体返回值情况可以看下面的代码分析
if (key == null) {
hash = 0;
index = indexOfNull();
} else {
// 计算出key的hash值并且计算出对应下标
hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
index = indexOf(key, hash);
}
// 如果下标大于0,也就是说key是存在的,那么直接更新对应的value
if (index >= 0) {
// 因为array是一个键值对占两个位置,比如0和1存放的是key和对应的value
// 所以这里对index左移一位并且+1
index = (index<<1) + 1;
// 把旧的值更新并且返回
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}
index = ~index;
// 判断现有长度和hash数组的长度,去进行扩容操作
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;
// 对mHashes mArray两个数组进行扩容操作
allocArrays(n);
// 检查长度避免多线程操作了map
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);
}
// 当下标是在中间的时候,把index后面(包括index)的所有数据往后移动一位
// 然后再把index对应的数据更新就达到了插入的操作
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();
}
}
// 更新对应index的值,把size+1记录个数
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++;
return null;
}
int indexOf(Object key, int hash) {
final int N = mSize;
// Important fast case: if nothing is in here, nothing to look for.
// 当前长度为0的时候直接对0取反,也就是返回了-1
if (N == 0) {
return ~0;
}
// 二分查找找到hash值要插入的下标
int index = binarySearchHashes(mHashes, N, hash);
// If the hash code wasn't found, then we have no entry for this key.
// binarySearchHashes里会把index取反,在put的时候也会再取反一次
if (index < 0) {
return index;
}
// If the key at the returned index matches, that's what we want.
// 如果根据找到的index在mArray里的key值也是相同的就可以直接返回
if (key.equals(mArray[index<<1])) {
return index;
}
// Search for a matching key after the index.
// 如果没找到,那么先向后查找再向前查找有没有对应的键值对存在
// 因为上面的二分查找有可能会漏了相同hash值的key,也就是说不同key但是hash值相同
// 这个时候直接用index去找可能会漏查,因为相同hash值的时候会插入到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.
// 返回 ~0
return ~end;
}
// 和indexof方法的区别在于代码的判断中把hash换成0,把key换成了null,其他逻辑不变
int indexOfNull() {
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, 0);
// 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 (null == mArray[index<<1]) {
return index;
}
// Search for a matching key after the index.
int end;
for (end = index + 1; end < N && mHashes[end] == 0; end++) {
if (null == mArray[end << 1]) return end;
}
// Search for a matching key before the index.
for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
if (null == 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;
}
依照代码可以看出,ArrayMap在put数据的时候会先将keyhash之后把找出hash值在hash数组的位置,然后对应的把value插入到对应的下标,而hash数组中的顺序是升序排的,所以mArray中的键值对不是按put的顺序。
上面还提到了一个扩容allocArrays以及缓存freeArrays的方法,也是ArrayMap一大特色
@SuppressWarnings("ArrayToString")
private void allocArrays(final int size) {
// 先判断size,因为arraymap只对4/8的长度做优化
if (size == (BASE_SIZE*2)) {
synchronized (SimpleArrayMap.class) {
// mTwiceBaseCache是一个缓存的数组,默认为null,如果有缓存的数组就拿来用
if (mTwiceBaseCache != null) {
// 先把mTwiceBaseCache保存到array
final Object[] array = mTwiceBaseCache;
// 把原本存放键值对的数组设为新的array,达到数组扩容的目的
mArray = array;
// 把缓存节点指向当前数组的第0个元素,把mHash数组指向第1个元素
mTwiceBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
// 再清空数组,这样mArray指向的就是一个长度为size的空数组
array[0] = array[1] = null;
mTwiceBaseCacheSize--;
if (DEBUG) System.out.println(TAG + " Retrieving 2x cache " + mHashes
+ " now have " + mTwiceBaseCacheSize + " entries");
return;
}
}
} else if (size == BASE_SIZE) {
// 逻辑同上
synchronized (SimpleArrayMap.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) System.out.println(TAG + " Retrieving 1x cache " + mHashes
+ " now have " + mBaseCacheSize + " entries");
return;
}
}
}
mHashes = new int[size];
mArray = new Object[size<<1];
}
@SuppressWarnings("ArrayToString")
private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
// 先判断传入长度,决定要不要进行缓存优化
if (hashes.length == (BASE_SIZE*2)) {
synchronized (SimpleArrayMap.class) {
// 达到缓存次数上限就不再缓存
if (mTwiceBaseCacheSize < CACHE_SIZE) {
// 把传入的数组第0存放当前缓存节点,把1用来存放hash数组
array[0] = mTwiceBaseCache;
array[1] = hashes;
// 把其他下标的数据清空
for (int i=(size<<1)-1; i>=2; i--) {
array[i] = null;
}
// 再把缓存节点指向传入的array
// 也就是说这里面是把传入的数组当成了新的缓存节点,然后把旧的缓存节点存放在了第0个元素
// hash数组放在第1个元素
mTwiceBaseCache = array;
mTwiceBaseCacheSize++;
if (DEBUG) System.out.println(TAG + " Storing 2x cache " + array
+ " now have " + mTwiceBaseCacheSize + " entries");
}
}
} else if (hashes.length == BASE_SIZE) {
// 逻辑同上
synchronized (SimpleArrayMap.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) System.out.println(TAG + " Storing 1x cache " + array
+ " now have " + mBaseCacheSize + " entries");
}
}
}
}
这里可能有的人会比较困惑这两个方法的逻辑和作用,实际上扩容以及缓存的逻辑分成以下几步
- put的过程中判断数组长度是否足够,如果需要扩容,会计算出扩容后的长度,然后先保存现有的
mArray和mHashs - 扩容的时候如果长度是符合缓存优化长度的,就会尝试去找缓存节点,如果有缓存数组
(用array代替)可以用,那就用临时变量把先取出0和1两个下标的元素保存起来,然后array的0和1设成null,这样就可以拿到一个符合长度的数组用来存放mArray的数据,然后下标1的数组给mHashs用,也就是是说原有的缓存节点数组中0会变成新的缓存节点,1会给mHash复用,整个数组array给mArray复用 - 当空余位置比较多的时候,会尝试缩短数组长度节省空间,同样先是判断当前有多少个数组,符合优化长度的才会试着去缓存,并且缓存次数不能超过
10次,然后把当前数组的0和1两个元素分别指向缓存节点以及当前的hash值数组,然后把其他元素清空,把缓存节点指向当前元素,也就是说每一次缓存会把缓存节点指向自身,并且自身array的0保存原有的缓存节点,1保存当前的hash数组
借用网上的一张图,缓存数组会保存上一个缓存数组节点以及当前hash数组,然后复用的时候就拿当前数组来用,这样通过下标1可以得到一个hash数组,然后清空之后就得到一个可以给mArray复用的数组,然后把缓存节点指向下标0更新缓存节点,这样就完成了缓存复用的流程。
而缓存的时候也是同样的方式,把mHash数组保存在了mArray数组的第1个元素中,把原有的缓存节点保存在下标0中,然后把缓存节点指向当前array,完成缓存数组的更新以及叠加。
一般情况下都是先扩容再检查缓存节点,所以相当于是给把数据换到一个扩容
size的新数组,然后把原本的数组加到缓存链中去。还有就是clearremove这种操作
3. remove操作
@Override
public V remove(Object key) {
// 找到对应key的下标之后再调用removeAt
final int index = indexOfKey(key);
if (index >= 0) {
return removeAt(index);
}
return null;
}
// 也是用到了indexOf和indexOfNull两个方法,具体逻辑在上面有提到
public int indexOfKey(Object key) {
return key == null ? indexOfNull()
: indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
}
public V removeAt(int index) {
// 先检查数据合法性
if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
// The array might be slightly bigger than mSize, in which case, indexing won't fail.
// Check if exception should be thrown outside of the critical path.
throw new ArrayIndexOutOfBoundsException(index);
}
final Object old = mArray[(index << 1) + 1];
final int osize = mSize;
final int nsize;
// 如果原有长度只有1/0,那直接清空数组和数据就行了,并且会检查一下当前数组是否可以添加到缓存中去
if (osize <= 1) {
// Now empty.
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;
// 判断一下map中数据长度和数组的长度,如果数据个数不到数组长度的1/3,那么重新分配数组
if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
// Shrunk enough to reduce size of arrays. We don't allow it to
// shrink smaller than (BASE_SIZE*2) to avoid flapping between
// that and BASE_SIZE.
// 重新计算数组长度节省空间
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);
// 检查数据避免多线程操作的时候出现问题
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
// 这里remove数据的方式是复制index前以及后的数据,跳过index
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);
}
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 {
// 这里逻辑是不涉及重新分配数组的,并且检查一下下标合法性
if (index < nsize) {
if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
+ " to " + index);
// 直接把index后的数据都复制往前移一位
System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
(nsize - index) << 1);
}
// 因为上面的操作把数据都复制了一份,这样会多出最后一个元素,所以要把最后一个设成null
// 这样就完成了对index的remove
mArray[nsize << 1] = null;
mArray[(nsize << 1) + 1] = null;
}
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
// 更新数据长度
mSize = nsize;
return (V)old;
}
4. append putAll
public void append(K key, V value) {
// append默认是添加到末尾
int index = mSize;
// 根据key算出hash值
final int hash = key == null ? 0
: (mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
// 检查index的合法性,做一些判断
if (index >= mHashes.length) {
throw new IllegalStateException("Array is full");
}
// 找到位置存放,但是hash值没有按升序排列的时候,会打印异常然后用put去存放
if (index > 0 && mHashes[index-1] > hash) {
RuntimeException e = new RuntimeException("here");
e.fillInStackTrace();
Log.w(TAG, "New hash " + hash
+ " is before end of array hash " + mHashes[index-1]
+ " at index " + index + " key " + key, e);
put(key, value);
return;
}
// 如果index合法并且hash值也合法,直接把数据加到末尾
mSize = index+1;
mHashes[index] = hash;
index <<= 1;
mArray[index] = key;
mArray[index+1] = value;
}
/**
* Perform a {@link #put(Object, Object)} of all key/value pairs in <var>array</var>
* @param array The array whose contents are to be retrieved.
*/
public void putAll(ArrayMap<? extends K, ? extends V> array) {
final int N = array.mSize;
// 先确定容量是否足够
ensureCapacity(mSize + N);
// map是空的时候直接把数据都复制过去
if (mSize == 0) {
if (N > 0) {
System.arraycopy(array.mHashes, 0, mHashes, 0, N);
System.arraycopy(array.mArray, 0, mArray, 0, N<<1);
mSize = N;
}
} else {
// 不为空的时候for循环使用put
for (int i=0; i<N; i++) {
put(array.keyAt(i), array.valueAt(i));
}
}
}
public void ensureCapacity(int minimumCapacity) {
final int osize = mSize;
if (mHashes.length < minimumCapacity) {
// 需要扩容的时候照样是保存旧数据然后去扩容,复制数据,接着缓存数组
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(minimumCapacity);
if (mSize > 0) {
System.arraycopy(ohashes, 0, mHashes, 0, osize);
System.arraycopy(oarray, 0, mArray, 0, osize<<1);
}
freeArrays(ohashes, oarray, osize);
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize != osize) {
throw new ConcurrentModificationException();
}
}
5. setValueAt
public V setValueAt(int index, V value) {
// 先检查index是否合法
if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
// The array might be slightly bigger than mSize, in which case, indexing won't fail.
// Check if exception should be thrown outside of the critical path.
throw new ArrayIndexOutOfBoundsException(index);
}
// 把对应index的value更新,然后返回旧的value,这里hash值不需要改变
index = (index << 1) + 1;
V old = (V)mArray[index];
mArray[index] = value;
return old;
}
总结
ArrayMap中有两个数组来存放数据,mArray存放键值对数据,mHashs存放键的hash值。- 查找下标的时候用二分法去找到插入的地方,
mHash中数据是按升序存放,所以mArray的键值对数据不是按存放顺序排。 ArrayMap中对应的会进行数组缓存优化,但是只针对容量为4/8的长度,所以如果初始化的时候用4/8会提高效率,但是也不一定就得用4/8,具体场景具体分析ArrayMap用一个变量存放无用的数组来作为缓存,并且可以重复连接来形成一个缓存链,最大缓存次数为10- 每次扩容的时候如果超出
8的长度,会变成原长度的1.5倍 ArrayMap为非线程安全类,所以代码里做了很多标记判断