SparseArray和ArrayMap

article/2025/3/4 14:46:22

首先我们来介绍一下HashMap,了解它的优缺点,然后再对比一下其他的数据结构以及为什么要替代它。

HashMap

HashMap是由数组+单向链表的方式组成的,初始大小是16(2的4次方),首次put的时候,才会真正初始化。

在这里插入图片描述
链表长度大于8时转化成红黑树,小于6时又转化成链表。
1.为什么要引入红黑树?
JDK 1.8以前是数组+链表,还未引入红黑树,这就导致了链表过长时查找的时间复杂度是O(n)

红黑树是一种自平衡的二叉查找树,不是一种绝对平衡的二叉树,它放弃了追求绝对平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。

2.为什么初始值大小为2的N次方,以后每次扩容后的结果也是2的N次方?
1.如果往hashmap中存放数据,我们首先得保证它能够尽量均匀分布,为了保证能够均匀分布,我们可能会想到用取模的方式去实现,如果用传统的‘%’方式来实现效率不高,当大小(length)总为2的n次方时,h&(length-1)运算等价于对length取模,也就是h%lenth,但是&比%具有更高的效率,同时也减少了hash碰撞。
2.和h&(length-1)相关,当容量大小(n)为2的n次方时,n**-1 的二进制的后几位全是1,在h为随机数的情况下,与(n-1)进行与操作时,会分布的更均匀**,想一想,如果n-1的二进制数是1110,当尾数为0时,与出来的值尾数永远为0,那么0001,1001,1101等尾数为1的位置就永远不可能被entry占用,就造成了空间浪费。

在hashmap源码中,put方法会调用indexFor方法,这个方法主要是根据key的hash值找到这个entry在table中的位置,最后 return 的是 h&(length-1)

3.HashMap和其他数据结构的关联
3.1与Hashtable的关系
Hashtable是线程安全的,比如put,get等很多使用了synchronized修饰,和ConcurrentHashMap相比,在Hashtable的大小增加到一定的时候,效率会急剧下降,因为迭代时需要被锁定很长的时间**(而ConcurrentHashMap引入了分割,仅仅需要锁定map的某个部分)**所以其实效率并不高

Hashtable的put方法

public synchronized V put(K key, V value) {// Make sure the value is not nullif (value == null) {throw new NullPointerException();}// Makes sure the key is not already in the hashtable.HashtableEntry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;@SuppressWarnings("unchecked")HashtableEntry<K,V> entry = (HashtableEntry<K,V>)tab[index];for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) && entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}addEntry(hash, key, value, index);return null;
}

里面的index是用%的方式进行取下标,效率不行
3.2 与HashSet的关系
我们先来看HashSet的add方法

public boolean add(E e) {return map.put(e, PRESENT)==null;

**HashSet底层就是HashMap,**HashSet不允许放入重复元素,虽然说Set对于重复的元素不放入,倒不如直接说是底层的Map直接把原值替代了。HashSet没有提供get方法,原因同HashMap一样,Set内部是无序的,只能通过迭代的方式获取。
在这里插入图片描述

与LinkedHashMap的关系,LinkedHashMap是一个数组+双向链表与HashMap不同,可以保证元素的迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序(LRU原理)。
与LinkedHashSet的关系,LinkedHashSet是继承自HashSet,底层实现是LinkedHashMap。
TreeSet中存放的元素是有序的(不是插入时的顺序,是有按关键字大小排序的),且元素不能重复。

3.4 链表长度大于8一定会转化成红黑树吗?

就算链表长度超过8,进入了这个转化为红黑树的方法,还是要判断当前容量的大小是否小于64,如果小于这个值还是要进行扩容而不是转化为红黑树

HashMap并发的问题

HashMap不是线程安全的,在Java7 中,HashMap被多个线程操作可能造成形成环形链表,造成死循环。可以用ConcurrentHashMap来解决,Java7 中ConcurrentHashMap 是用分段锁的方式实现的,也就是二级哈希表。在Java8 中的实现方式是通过Unsafe类的CAS自旋赋值+synchronized同步+LockSupport阻塞等手段实现的高效并发,代码可读性稍差。最大的区别在于JDK8的锁粒度更细,理想情况下talbe数组元素的大小就是其支持并发的最大个数。

HashMap的缺点,扩容因子为0.75(数据论文表示0.6~0.75性能最好),同时默认每次扩容都是2倍进行扩容,有点浪费空间(如果只是超过了一个),其实是以空间换时间。

SparseArray

SparseArray中Key为int类型(避免了装箱和拆箱),Value是Object类型,Key和Value分别存放在一个数组内,Key数组int值是按顺序排列的,查找的时候采用的是二分查找,效率很高。而Value数组的位置和Key数组中的位置是一样的。

add的时候会进行位移,remove的时候不一定会进行位移,把某个值标记为delete,如果下次有符合的值直接放到该位置,就没有位移了。但是也有缺点,Key只能是int值。最后Android中还扩展了SparseLongArray。

ArrayMap

ArrayMap的Key和Value同HashMap一样都可以存放多种类型,ArrayMap对象的数据存储格式如下:

1.mHashes是一个记录所有key的hashcode值组成的数组,是从小到大的排序方式;采用二分查找法,从mHashes数组中查找值等于hash的key
2.mArray是一个记录着key-value键值对所组成的数组,是mHashes大小的2倍;
在这里插入图片描述

ArrayMap中有两个非常重要的静态成员变量mBaseCache和mTwiceBaseCacheSize,用于ArrayMap所在进程的全局缓存功能:

mBaseCache:用于缓存大小为4的ArrayMap,mBaseCacheSize记录着当前已缓存的数量,超过10个则不再缓存;
mTwiceBaseCacheSize:用于缓存大小为8的ArrayMap,mTwiceBaseCacheSize记录着当前已缓存的数量,超过10个则不再缓存。

很多场景可能起初都是数据很少,为了减少频繁地创建和回收Map对象,ArrayMap采用了两个大小为10的缓存队列来分别保存大小为4和8的Map对象。为了节省内存有更加保守的内存扩张以及内存收缩策略。

内存释放freeArrays()触发时机:

当执行removeAt()移除最后一个元素的情况
当执行clear()清理的情况
当执行ensureCapacity()在当前容量小于预期容量的情况下, 先执行allocArrays,再执行freeArrays
当执行put()在容量满的情况下, 先执行allocArrays, 再执行freeArrays

private static void freeArrays(final int[] hashes, final Object[] array, final int size) {if (hashes.length == (BASE_SIZE*2)) {  //当释放的是大小为8的对象synchronized (ArrayMap.class) {// 当大小为8的缓存池的数量小于10个,则将其放入缓存池if (mTwiceBaseCacheSize < CACHE_SIZE) { array[0] = mTwiceBaseCache;  //array[0]指向原来的缓存池array[1] = hashes;for (int i=(size<<1)-1; i>=2; i--) {array[i] = null;  //清空其他数据}mTwiceBaseCache = array; //mTwiceBaseCache指向新加入缓存池的arraymTwiceBaseCacheSize++; }}} else if (hashes.length == BASE_SIZE) {  //当释放的是大小为4的对象,原理同上synchronized (ArrayMap.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++;}}}
}

最初mTwiceBaseCache和mBaseCache缓存池中都没有数据,在freeArrays释放内存时,如果同时满足释放的array大小等于4或者8,且相对应的缓冲池个数未达上限,则会把该arrya加入到缓存池中。**加入的方式是将数组array的第0个元素指向原有的缓存池,第1个元素指向hashes数组的地址,第2个元素以后的数据全部置为null。再把缓存池的头部指向最新的array的位置,并将该缓存池大小执行加1操作。**具体如下所示。
在这里插入图片描述

内存分配allocArrays触发时机:
当执行ArrayMap的构造函数的情况
当执行removeAt()在满足容量收紧机制的情况
当执行ensureCapacity()在当前容量小于预期容量的情况下, 先执行allocArrays,再执行freeArrays
当执行put()在容量满的情况下, 先执行allocArrays, 再执行freeArrays

当allocArrays分配内存时,如果所需要分配的大小等于4或者8,且相对应的缓冲池不为空,则会从相应缓存池中取出缓存的mArray和mHashes。从缓存池取出缓存的方式是将当前缓存池赋值给mArray,将缓存池指向上一条缓存地址,将缓存池的第1个元素赋值为mHashes,再把mArray的第0和第1个位置的数据置为null,并将该缓存池大小执行减1操作,具体如下所示。
在这里插入图片描述

ArrayMap中解决Hash冲突的方式是追加的方式,比如两个key的hash(int)值一样,那就把数据全部后移一位,通过追加的方式解决Hash冲突。

ArrayMap的扩容

当mSize大于或等于mHashes数组长度时则扩容,完成扩容后需要将老的数组拷贝到新分配的数组,并释放老的内存。

当map个数满足条件 osize<4时,则扩容后的大小为4;
当map个数满足条件 4<= osize < 8时,则扩容后的大小为8;
当map个数满足条件 osize>=8时,则扩容后的大小为原来的1.5倍;

当数组内存的大小大于8,且已存储数据的个数mSize小于数组空间大小的1/3的情况下,需要收紧数据的内容容量,分配新的数组,老的内存靠虚拟机自动回收。

如果mSize<=8,则设置新大小为8;
如果mSize> 8,则设置新大小为mSize的1.5倍。
也就是说在数据较大的情况下,当内存使用量不足1/3的情况下,内存数组会收紧50%。

ArrayMap也是非线程安全的类


http://chatgpt.dhexx.cn/article/OeE5lMgH.shtml

相关文章

SparseArray与ArrayMap

SparseArray SparseArray核心代码 两个构造函数默认数组容量10 public SparseArray() {this(10); } public SparseArray(int initialCapacity) {if (initialCapacity 0) {mKeys EmptyArray.INT;mValues EmptyArray.OBJECT;} else {mValues ArrayUtils.newUnpaddedObjectA…

arraymap android,深入剖析 Android中的 ArrayMap

数据集合在任何一门编程语言中都是很重要的一部分&#xff0c;在 Android 开发中&#xff0c;我们会实用到ArrayList, LinkedList, HashMap等。其中HashMap是用来处理键值对需求的常用集合。 而Android中引入了一个新的集合&#xff0c;叫做ArrayMap&#xff0c;为键值对存储需…

ArrayMap 笔记整理

源码基于 API 25 主要参考文章&#xff1a;面试必备&#xff1a;ArrayMap源码解析 1、概述 截图自&#xff1a;面试必备&#xff1a;ArrayMap源码解析 在开始讲解源码之前&#xff0c;需要说明 ArrayMap 的底层实现结构&#xff0c;即两个数组&#xff1a; int[] mHashes; /…

ArrayMap 原理

一 概述 在移动设备端&#xff0c;内存资源很珍贵&#xff0c;HashMap 为实现快速查询带来了很大内存的浪费。为此&#xff0c;2013年5月20日 Google 工程师 Dianne Hackborn 在 Android 系统源码中新增 ArrayMap 类&#xff0c;从 Android 源码中发现有不少提交&#xff0c;专…

不再害怕面试问ArrayMap一文完全看懂Android ArrayMap源码解析

作者&#xff1a;VIjolie 前言 ArrayMap是谷歌推出的在安卓等设备上用于替代HashMap的数据结构&#xff0c;和HashMap相比&#xff0c;具有更高的内存使用率&#xff0c;因此适合在Android等内存较为紧张的移动设备&#xff0c;下面结合源码分析ArrayMap实现原理&#xff0c;主…

ArrayMap原理解析

1.ArrayMap是什么 一个通用的key-value映射数据结构 相比HashMap会占用更少的内存空间 android.util和android.support.v4.util都包含对应的ArrayMap类 2.为什么要使用ArrayMap ArrayMap是一个普通的键值映射的数据结构&#xff0c;这种数据结构比传统的HashMap有着更好的内…

ArrayMap源码解析

一、数据结构 ArrayMap是一个key-value的数据结构&#xff0c;它比HashMap有更高的内存效率 它映射到两个数组结构&#xff1a;一个整数数组mHashes&#xff0c;用来保存key的hashcode&#xff1b;一个对象数组mArray&#xff0c;保存key-value 它不适用于大量数据的存储&…

ArrayMap的使用与详解

数据集合在任何一门编程语言中都是很重要的一部分&#xff0c;在 Android 开发中&#xff0c;我们会实用到List,ArrayList, HashMap等。List和ArrayList配合使用&#xff0c;其中HashMap是用来处理键值对需求的常用集合。 而Android中引入了一个新的集合&#xff0c;叫做ArrayM…

ipdb 调试 - 终端显示正常,日志显示乱码

问题描述 ipdb调试模型&#xff0c;在 vscode 终端正常显示&#xff0c;但日志文件中&#xff0c;输入流将ipdb调试信息的颜色代码记录在文件中&#xff0c;日志文件无法查阅。 解决方案&#xff1a; 提示&#xff1a;修改ipdb的终端颜色属性 点击 ipdb&#xff0c;进入 ipdb …

ipdb调试dataloader

ipdb不可以直接放入__init__/__len__/__getitem__进行调试 需要在主函数中中断调试&#xff0c;例如colab中&#xff1a; 输入需要查询的变量即可

初次使用python(2)之如何下载ipdb模块

上次提到了ipdb这个python中用于进行调试的模块&#xff0c;但在python中是额外的package。所以&#xff0c;这篇文章写得是如何下载ipdb模块。 在Windows环境中&#xff0c;需要用pip来下载所有模块。 1.安装pip 我们仍需要在python官网上找到pip文件&#xff0c;下载地址是&a…

python ipdb 调试代码

python ipdb 调试代码 安装 pip install ipdb使用 第一种方法 python -m ipdb xxx.py #单步调试也可以写一个.py文件&#xff0c;如下&#xff0c;来执行。 import os os.system(python -m ipdb xxx.py)第二种方法 在需要断点的地方插入两句话 from ipdb import set_tra…

python debug ipdb

pudb使用 to be continued… ipdb和pdb区别 实际上ipdb是pdb的扩展版本&#xff0c;在pdb的基础上添加了如下功能&#xff1a; 可以使用tab&#xff08;提示&#xff09;补全代码的功能&#xff08;我觉得这一点上我就完全倒戈了…&#xff09;调试不再是黑白的&#xff0c…

Python调试pdb和ipdb

什么是pdb和ipdb 不知道大家在用Python写代码出现报错时是怎样调试的&#xff0c;从报错提示定位回去一步一步check每一行&#xff1f;如果没有IDE或者命令行写代码时又该怎样快速调试&#xff1f;这时如果使用pdb进行调试将会异常方便。 Pdb就是Python debugger&#xff0c;…

python调试模块ipdb

1. 调试python ipdb是用来python中用以交互式debug的模块&#xff0c;可以直接利用pip安装; 其功能类似于pycharm中 python控制台&#xff0c; 而使用ipdb 的优点&#xff0c;便是直接在代码中调试&#xff0c; 避免了在python控制台&#xff0c;或者重新设置一些简单变量。…

python调试器 ipdb

文章目录 1. 介绍1.1 常用调试方式1.2 安装 ipdb 2. 用法3. 命令3.1、查看源代码3.2、添加断点3.3 添加临时断点3.4 清除断点3.5、打印变量值3.6、逐行调试命令3.7、非逐行调试命令3.8 跳出函数&#xff0c;跳入函数3.9、查看当前函数所有参数3.10 打印变量的值3.11、打印变量类…

ipdb python下的debug利器

ipdb是一个IPython的python下debug神器&#xff0c;支持在代码中添加断点&#xff0c;进行单步调试&#xff0c;支持在调试过程中查看各种变量的值&#xff0c;新增修改断点&#xff0c;或跳过特定断点。以往进行python代码的调试都是通过pycharm或vscode添加断点&#xff0c;在…

js中函数传参的问题

在使用ajax或者js拼接信息在页面中显示的时候&#xff0c;需要向函数传参的时候往往会有问题。 如果传递的是数字类型&#xff0c;则是可以直接传递。不需要进行转义。 如果是字符串类型&#xff0c;则需要进行转义&#xff0c;才会以存入字符串的参数。

js函数传参

也许大家对于函数的参数都不会太在意&#xff0c;简单来说&#xff0c;把函数外部的值复制给函数内部的参数&#xff0c;就和把值从一个变量复制到另一个变量一样。深入研究&#xff0c;你会发现其实没那么简单&#xff0c;这个传参是要分俩种情况&#xff08;其实这是个错误的…