我知道的:
这三个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