android 替代map,Android为什么推荐使用SparseArray来替代HashMap?

SparseArray也许你没听过,那正好今天就来学习一下咯,这也是Android官方推荐使用的,所以我们需要了解一下他的优势和劣势在哪些地方。

首先SparseArray用来和HashMap做比较,在安卓项目中,你新建一个HashMap对象,注意下面会有下划线,里面有提示翻译成白话文的意思就是建议使用SparseArray替代HashMap来获得更好的表现。我们都知道HashMap在java中很常用,并且也很吃香,面试官经常会问到HashMap源码实现,既然HashMap的表现都这么牛逼了,那么为什么Android官方会推荐使用SparseArray呢?不要急,慢慢来~

首先来了解一下SparseArray,翻译过来称为“稀疏数组”,所谓稀疏数组就是数组中大部分的内容值都未被使用(或都为零),在数组中仅有少部分的空间使用。因此造成内存空间的浪费,为了节省内存空间,并且不影响数组中原有的内容值,我们可以采用一种压缩的方式来表示稀疏数组的内容。

下面有网上举例子的两张图,我挪过来了,如果涉及侵权问题,请联系我删除哈,毕竟自己照猫画虎重新画一个,浪费时间,没有必要:可以看出来这是一个9*7的数组,它所占用的空间为63个,但是如图只有5个空间使用了,其他的都是浪费,浪费是可耻的,所以按照稀疏数组的整理,就变成了下面这张图所示大家应该都能看懂吧,二维数组对应的值,那么这么一来所占空间仅仅为3*6=18个了,相比起来是否节省很多,所以这就是为什么Android官方推荐使用SparseArray的原因了,我们稍后来验证。

下面我们来写一段程序来看看SparseArray和HashMap在处理数据上面的性能情况。HashMap正向插入数据HashMapmap=newHashMap();longstart=System.currentTimeMillis();for(inti=0;i<100000;i++){map.put(i,String.valueOf(i));}longmemory=Runtime.getRuntime().totalMemory();longend=System.currentTimeMillis();longusedTime=end-start;System.out.println("HashMap消耗的时间是:"+usedTime+",HashMap占用的内存是:"+memory);

输出结果:SparseArray正向插入数据SparseArraysparse=newSparseArray<>();longstart=System.currentTimeMillis();for(inti=0;i<100000;i++){sparse.put(i,String.valueOf(i));}longmemory=Runtime.getRuntime().totalMemory();longend=System.currentTimeMillis();longusedTime=end-start;System.out.println("SparseArray消耗的时间是:"+usedTime+",SparseArray占用的内存是:"+memory);

输出结果:

公平起见,这两段代码我都是分别运行在Android程序中的,一次说明不了什么结果,所以我重复启动了几次项目,这个时候再做对比就很明显了,这四次结果SpaseArray的内存占用都是明显低于HashMap的内存占用的,消耗的时间好像也是这样的,但是Android官方并没有提到效率这个问题,我们之前分析SpaseArray的内存结果也没有发现这个现象,那么这是为什么呢?不要急,下面我们反向来插入一次数据再做一次比较。HashMap反向插入数据HashMapmap=newHashMap();longstart=System.currentTimeMillis();for(inti=100000-1;i>=0;i--){map.put(i,String.valueOf(100000-i-1));}longmemory=Runtime.getRuntime().totalMemory();longend=System.currentTimeMillis();longusedTime=end-start;System.out.println("HashMap消耗的时间是:"+usedTime+",HashMap占用的内存是:"+memory);

输出结果:SparseArray反向插入数据SparseArraysparse=newSparseArray<>();longstart=System.currentTimeMillis();for(inti=100000-1;i>=0;i--){sparse.put(i,String.valueOf(100000-i-1));}longmemory=Runtime.getRuntime().totalMemory();longend=System.currentTimeMillis();longusedTime=end-start;System.out.println("SparseArray消耗的时间是:"+usedTime+",SparseArray占用的内存是:"+memory);

输出结果:

运行了三次以后我都不好意思再运行了,可以看到倒入的情况下,消耗时间表现非常糟糕,并且页面这个时候处于卡顿状态,但是仍然有一点,那就是SparseArray占用的内存依然比HashMap优秀的多,由于SparseArray每次在插入的时候都要使用二分查找判断是否有相同的值被插入.因此这种倒序的情况是SparseArray效率最差的时候。

源码分析

打开SparseArray源码,我们先看看左边的结构视图有哪些方法:看看开头定义的变量privatestaticfinalObjectDELETED=newObject();privatebooleanmGarbage=false;privateint[]mKeys;privateObject[]mValues;privateintmSize;

很直接的可以看到有两个数组,并且他们的命名都那么的直接,一个存放key,一个存放value,还有一个大小。看看构造函数publicSparseArray(){this(10);}publicSparseArray(intinitialCapacity){if(initialCapacity==0){mKeys=EmptyArray.INT;mValues=EmptyArray.OBJECT;}else{mValues=ArrayUtils.newUnpaddedObjectArray(initialCapacity);mKeys=newint[mValues.length];}mSize=0;}

默认的构造函数也会调用重载方法并给初始容量设为10。看看插入方法put/***Addsamappingfromthespecifiedkeytothespecifiedvalue,*replacingthepreviousmappingfromthespecifiedkeyifthere*wasone.*///注释已经说明了如果key不存在,则插入key和value,如果key存在,则替换原来的value//所以这个方法也可以当作修改publicvoidput(intkey,Evalue){//通过二分法查找keyinti=ContainerHelpers.binarySearch(mKeys,mSize,key);if(i>=0){//如果i>0,说明之前已经存在了这个key,所以直接覆盖原来的valuemValues[i]=value;}else{//如果i<0,说明key不存在,所以i肯定是负数,所以我们要取相反数i=~i;//如果i在mSize范围之内并且之前的值被删除了,那么直接进行赋值即可if(i=mKeys.length){gc();//Searchagainbecauseindicesmayhavechanged.//回收完后引用可能改变了,所以需要对key重新二分法查找并取反i=~ContainerHelpers.binarySearch(mKeys,mSize,key);}mKeys=GrowingArrayUtils.insert(mKeys,mSize,i,key);mValues=GrowingArrayUtils.insert(mValues,mSize,i,value);mSize++;}}看看插入方法append/***Putsakey/valuepairintothearray,optimizingforthecasewhere*thekeyisgreaterthanallexistingkeysinthearray.*/publicvoidappend(intkey,Evalue){if(mSize!=0&&key<=mKeys[mSize-1]){put(key,value);return;}if(mGarbage&&mSize>=mKeys.length){gc();}mKeys=GrowingArrayUtils.append(mKeys,mSize,key);mValues=GrowingArrayUtils.append(mValues,mSize,value);mSize++;}

可以看出来,append方法其实很简单,它也是调用了put方法完成键值对的插入,并且调用回收的逻辑也是一样的。看看删除的4种方法

1.delete(intkey)/***Removesthemappingfromthespecifiedkey,iftherewasany.*/publicvoiddelete(intkey){inti=ContainerHelpers.binarySearch(mKeys,mSize,key);if(i>=0){if(mValues[i]!=DELETED){mValues[i]=DELETED;mGarbage=true;}}}

如果通过二分法能找到key,就将对应的索引对应的value设置为DELETED常量,并且标识mGarbage=true。

2.remove(intkey)publicvoidremove(intkey){delete(key);}

直接调用delete方法

3.removeAt(intindex)publicvoidremoveAt(intindex){if(mValues[index]!=DELETED){mValues[index]=DELETED;mGarbage=true;}}

原理和delete方法一样

4.clear()publicvoidclear(){intn=mSize;Object[]values=mValues;for(inti=0;i

这个方法顾名思义,将数组内容全部清空置为null。看看修改的方法setValueAt(intindex,Evalue)publicvoidsetValueAt(intindex,Evalue){if(mGarbage){gc();}mValues[index]=value;}

如果标记为垃圾的话,先进行一次垃圾回收,然后再将指定的值设置到对应的索引上。看看获取方法get()、get(intkey,EvalueIfKeyNotFound)publicEget(intkey){returnget(key,null);}publicEget(intkey,EvalueIfKeyNotFound){inti=ContainerHelpers.binarySearch(mKeys,mSize,key);if(i<0||mValues[i]==DELETED){returnvalueIfKeyNotFound;}else{return(E)mValues[i];}}

其实最终调用的都是get(intkey,EvalueIfKeyNotFound)方法,只不过第一个找不到key对应的value时返回的是null,第二个方法则可以由我们自己指定的默认值。

总结

大家一定要注意,我们插入的key并不是索引的角标,不要以为是int型就是角标了,那就错了,其实和map一样,都是通过key来查找value,我们传入key通过二分法生成一个index,这个index才是角标。

注意内存二字很重要,因为它仅仅提高内存效率,而不是提高执行效率,所以也决定它只适用于android系统(内存对android项目有多重要,地球人都知道)

最后引用别人总结的比较好的一段话来结束这篇文章:

SparseArray有两个优点:1.避免了自动装箱(auto-boxing),2.数据结构不会依赖于外部对象映射。我们知道HashMap采用一种所谓的“Hash算法”来决定每个元素的存储位置,存放的都是数组元素的引用,通过每个对象的hash值来映射对象。而SparseArray则是用数组数据结构来保存映射,然后通过折半查找来找到对象。但其实一般来说,SparseArray执行效率比HashMap要慢一点,因为查找需要折半查找,而添加删除则需要在数组中执行,而HashMap都是通过外部映射。但相对来说影响不大,最主要是SparseArray不需要开辟内存空间来额外存储外部映射,从而节省内存。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值