HashMap,ArrayMap,SparseArray 源码角度分析,Android中的数据结构你该如何去选择?

本文详细分析了HashMap、ArrayMap和SparseArray在Android中的使用,比较了它们的初始化策略、查找/插入效率、扩容机制以及内存占用。指出在特定场景下,如数据量小或对内存敏感时,应选择ArrayMap或SparseArray以优化性能。
摘要由CSDN通过智能技术生成

}

return null;

}

在get的时候,我们首先会根据我们的key去计算它的hash值,如果这个hash值不存在,我们直接反回null。

如果存在,在没有发生hash冲突的情况下也就是根据当前hash值计算出的索引上的存储数据不是以树和链表的形式存储的时候,我们直接返回当前索引上存储的值,如果时链表树,我们就去遍历节点上的数据通过equals去比对,找到我们需要的在返回。

通过上面我可以得出结论,当HashMap没有发生hash冲突时,hashMap的查找和插入的时间复杂度都是O(1),效率时非常高的。

当我们发生扩容和hash冲突时,会带来一定性能上的损耗。

HashMap大致分析完了。

下面我们来分析分析Android为我们提供的ArrayMap和SparseArray。

二.我们在来看看ArrayMap:


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的构造方法:

public ArrayMap(int capacity) {

super(capacity);

}

public SimpleArrayMap() {

mHashes = ContainerHelpers.EMPTY_INTS;

mArray = ContainerHelpers.EMPTY_OBJECTS;

mSize = 0;

}

我们发现ArrayMap的初始化会给我们初始化两个空数组,并不像HashMap一样为我们默认初始化了一个大小为16的table数组,下面我们继续往下看:

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方法:

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方法。

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默认初始化了两个空数组)。

if (osize >= mHashes.length)

出现hash冲突后,如果我们的存储数据数量大小已经大于等于我们的hash数组的大小。我们对数组进行扩容。

final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
(osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

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:


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方法。

public static 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)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的源码我们已经分析完了。下面我们来做一个总结。

四.结论


 HashMapArrayMapSparseArray
初始化初始化默认大小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

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

《设计思想解读开源框架》

第一章、 热修复设计

  • 第一节、 AOT/JIT & dexopt 与 dex2oat

  • 第二节、 热修复设计之 CLASS_ISPREVERIFIED 问题

  • 第三节、热修复设计之热修复原理

  • 第四节、Tinker 的集成与使用(自动补丁包生成)

    第二章、 插件化框架设计

  • 第一节、 Class 文件与 Dex 文件的结构解读

  • 第二节、 Android 资源加载机制详解

  • 第三节、 四大组件调用原理

  • 第四节、 so 文件加载机制

  • 第五节、 Android 系统服务实现原理

    第三章、 组件化框架设计

  • 第一节、阿里巴巴开源路由框——ARouter 原理分析

  • 第二节、APT 编译时期自动生成代码&动态类加载

  • 第三节、 Java SPI 机制

  • 第四节、 AOP&IOC

  • 第五节、 手写组件化架构

    第四章、图片加载框架

  • 第一节、图片加载框架选型

  • 第二节、Glide 原理分析

  • 第三节、手写图片加载框架实战

    第五章、网络访问框架设计

  • 第一节、网络通信必备基础

  • 第二节、OkHttp 源码解读

  • 第三节、Retrofit 源码解析

    第六章、 RXJava 响应式编程框架设计

  • 第一节、链式调用

  • 第二节、 扩展的观察者模式

  • 第三节、事件变换设计

  • 第四节、Scheduler 线程控制

    第七章、 IOC 架构设计

  • 第一节、 依赖注入与控制反转

  • 第二节、ButterKnife 原理上篇、中篇、下篇

  • 第三节、Dagger 架构设计核心解密

    第八章、 Android 架构组件 Jetpack

  • 第一节、 LiveData 原理

  • 第二节、 Navigation 如何解决 tabLayout 问题

  • 第三节、 ViewModel 如何感知 View 生命周期及内核原理

  • 第四节、 Room 架构方式方法

  • 第五节、 dataBinding 为什么能够支持 MVVM

  • 第六节、 WorkManager 内核揭秘

  • 第七节、 Lifecycles 生命周期


    本文包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

外链图片转存中…(img-EyoGsSJu-1712654612397)]

第七章、 IOC 架构设计

  • 第一节、 依赖注入与控制反转

  • 第二节、ButterKnife 原理上篇、中篇、下篇

  • 第三节、Dagger 架构设计核心解密

    [外链图片转存中…(img-5FVKTD4j-1712654612397)]

    第八章、 Android 架构组件 Jetpack

  • 第一节、 LiveData 原理

  • 第二节、 Navigation 如何解决 tabLayout 问题

  • 第三节、 ViewModel 如何感知 View 生命周期及内核原理

  • 第四节、 Room 架构方式方法

  • 第五节、 dataBinding 为什么能够支持 MVVM

  • 第六节、 WorkManager 内核揭秘

  • 第七节、 Lifecycles 生命周期

    [外链图片转存中…(img-2n6KURve-1712654612397)]
    本文包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
    [外链图片转存中…(img-6qPdAgZh-1712654612397)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值