SparseArray 与 HashMap
Posted
技术标签:
【中文标题】SparseArray 与 HashMap【英文标题】:SparseArray vs HashMap 【发布时间】:2014-10-23 00:20:16 【问题描述】:我可以想到几个原因,为什么带有整数键的HashMap
s 比SparseArray
s 好得多:
SparseArray
的 android 文档说“它通常比传统的 HashMap
慢”。
如果您使用HashMap
s 而不是SparseArray
s 编写代码,您的代码将适用于地图的其他实现,并且您将能够使用为地图设计的所有Java API。
如果您使用HashMap
s 而不是SparseArray
s 编写代码,您的代码将在非android 项目中工作。
地图会覆盖equals()
和hashCode()
,而SparseArray
不会。
然而,每当我尝试在 Android 项目中使用带有整数键的 HashMap
时,IntelliJ 都会告诉我应该改用 SparseArray
。我觉得这真的很难理解。有人知道使用SparseArray
s 的任何令人信服的理由吗?
【问题讨论】:
【参考方案1】:SparseArray
可用于当键是原始类型时替换 HashMap
。
不同的键/值类型有一些变体,尽管并非所有变体都是公开可用的。
好处是:
免分配 没有拳击缺点:
通常较慢,不适用于大型集合 它们不适用于非 Android 项目HashMap
可以替换为:
SparseArray <Integer, Object>
SparseBooleanArray <Integer, Boolean>
SparseIntArray <Integer, Integer>
SparseLongArray <Integer, Long>
LongSparseArray <Long, Object>
LongSparseLongArray <Long, Long> //this is not a public class
//but can be copied from Android source code
在内存方面,这里是 SparseIntArray
与 HashMap<Integer, Integer>
的 1000 个元素的示例:
SparseIntArray
:
class SparseIntArray
int[] keys;
int[] values;
int size;
类 = 12 + 3 * 4 = 24 个字节 数组 = 20 + 1000 * 4 = 4024 字节 总计 = 8,072 字节
HashMap
:
class HashMap<K, V>
Entry<K, V>[] table;
Entry<K, V> forNull;
int size;
int modCount;
int threshold;
Set<K> keys
Set<Entry<K, V>> entries;
Collection<V> values;
类 = 12 + 8 * 4 = 48 个字节 条目 = 32 + 16 + 16 = 64 字节 数组 = 20 + 1000 * 64 = 64024 字节 总计 = 64,136 字节
来源:Android Memories by Romain Guy 来自幻灯片 90。
上面的数字是 JVM 在堆上分配的内存量(以字节为单位)。 它们可能因使用的特定 JVM 而异。
java.lang.instrument
包包含一些对高级操作有用的方法,例如使用getObjectSize(Object objectToSize)
检查对象的大小。
更多信息可从官方Oracle documentation获得。
类 = 12 字节 +(n 个实例变量)* 4 字节 数组 = 20 字节 +(n 个元素)*(元素大小) 条目 = 32 字节 +(第一个元素大小)+(第二个元素大小)
【讨论】:
谁能指导我这些“12 + 3 * 4”和“20 + 1000 * 4”是从哪里来的? @MarianPaździoch,他展示了一个演示文稿(speakerdeck.com/romainguy/android-memories),其中一个类占用 12 个字节 + 3 个 4 个字节的变量,一个数组(引用)占用 20 个字节(dlmalloc - 4,对象开销 - 8 , width&padding - 8). 为了记录,SparseArray 的另一个主要缺点是,作为一个 Android 对象,它需要被模拟以进行单元测试。我现在尽可能使用 Java 自己的对象来简化测试。 @DavidG 你可以使用unmock plugin来模拟android依赖。 即使你不是在做Android,将类复制到你的项目中并不难,它只依赖于其他3个类。 APL 许可证意味着可以这样做,无论您使用什么许可证。【参考方案2】:我来到这里只是想要一个如何使用SparseArray
的示例。这是对此的补充答案。
创建一个稀疏数组
SparseArray<String> sparseArray = new SparseArray<>();
SparseArray
将整数映射到某个Object
,因此您可以将上面示例中的String
替换为任何其他Object
。如果您要将整数映射到整数,请使用SparseIntArray
。
添加或更新项目
使用put
(或append
)向数组中添加元素。
sparseArray.put(10, "horse");
sparseArray.put(3, "cow");
sparseArray.put(1, "camel");
sparseArray.put(99, "sheep");
sparseArray.put(30, "goat");
sparseArray.put(17, "pig");
请注意,int
键不需要按顺序排列。这也可用于更改特定 int
键的值。
删除项目
使用remove
(或delete
)从数组中删除元素。
sparseArray.remove(17); // "pig" removed
int
参数是整数键。
查找 int 键的值
使用get
获取某个整数键的值。
String someAnimal = sparseArray.get(99); // "sheep"
String anotherAnimal = sparseArray.get(200); // null
如果您想避免因缺少密钥而收到null
,您可以使用get(int key, E valueIfKeyNotFound)
。
迭代项目
您可以使用keyAt
和valueAt
一些索引来循环遍历集合,因为SparseArray
维护一个与int
键不同的单独索引。
int size = sparseArray.size();
for (int i = 0; i < size; i++)
int key = sparseArray.keyAt(i);
String value = sparseArray.valueAt(i);
Log.i("TAG", "key: " + key + " value: " + value);
// key: 1 value: camel
// key: 3 value: cow
// key: 10 value: horse
// key: 30 value: goat
// key: 99 value: sheep
请注意,键是按升序排列的,而不是按它们添加的顺序。
【讨论】:
【参考方案3】:然而,每当我尝试在 android 中使用带有整数键的 HashMap 项目中,intelliJ 告诉我应该使用 SparseArray。
这只是来自这个稀疏数组的documentation 的警告:
它旨在比使用 HashMap 更节省内存 将整数映射到对象
SparseArray
比使用常规 HashMap 更内存效率,即不允许在数组中出现多个间隙,不像 HashMap。如果您不想担心设备的内存分配问题,您可以使用传统的 HashMap。
【讨论】:
关于节省内存的观点显然是有效的,但我一直不明白为什么android不能让 SparseArraySparseArray
防止关键整数是Auto box,这是另一种操作和性价比。而不是 Map 它将原始整数自动装箱到Integer
也是正确的,但是如果他们通过包含一个带有签名 put(int a, T t) 的 put 方法来重载 put 方法,那么您仍然可以将键值对放入没有键的映射中被自动装箱。我只是认为集合框架是如此强大(使用 Java 的最佳理由之一)以至于不利用它是疯狂的。
@PaulBoddington 集合基于对象而不是原始对象,因此它无法在集合 API 中工作【参考方案4】:
经过一番谷歌搜索后,我尝试在已发布的答案中添加一些信息:
Isaac Taylor 对 SparseArrays 和 Hashmaps 进行了性能比较。他说
Hashmap 和 SparseArray 的数据结构非常相似 尺寸小于 1,000
和
当大小增加到 10,000 标记时 [...] Hashmap 在添加对象时具有更高的性能,而 SparseArray 具有 检索对象时性能更高。 [...] 大小为 100,000 [...] Hashmap 会很快失去性能
对Edgblog 的比较表明,由于键更小(int vs Integer),SparseArray 需要的内存比 HashMap 少得多,而且
HashMap.Entry 实例必须跟踪 键、值和下一个条目。另外它还需要存储 条目的哈希为 int。
作为结论,如果您要在地图中存储大量数据,那么差异可能很重要。否则,请忽略警告。
【讨论】:
【参考方案5】:Java 中的稀疏数组是一种将键映射到值的数据结构。与 Map 的想法相同,但实现方式不同:
Map 在内部表示为一个列表数组,其中这些列表中的每个元素都是一个键值对。键和值都是对象实例。
稀疏数组由两个数组组成:一个(基元)键数组和一个(对象)值数组。这些数组索引中可能存在间隙,因此称为“稀疏”数组。
SparseArray 的主要优点在于它通过使用基元而不是对象作为键来节省内存。
【讨论】:
【参考方案6】:SparseArray 的 android 文档说“它通常是 比传统的 HashMap 慢”。
是的,没错。但是当您只有 10 或 20 个项目时,性能差异应该是微不足道的。
如果您使用 HashMaps 而不是 SparseArrays 编写代码,您的代码 将与 Map 的其他实现一起使用,您将能够 使用为地图设计的所有 Java API
我认为大多数情况下我们只使用HashMap
来搜索与键关联的值,而SparseArray
非常擅长这一点。
如果您使用 HashMaps 而不是 SparseArrays 编写代码,您的代码 将在非android项目中工作。
SparseArray 的源代码相当简单易懂,因此您只需花费很少的精力将其移动到其他平台(通过简单的复制和粘贴)。
Map 覆盖 equals() 和 hashCode() 而 SparseArray 没有
我只能说,(对大多数开发人员)谁在乎?
SparseArray
的另一个重要方面是它只使用一个数组来存储所有元素,而HashMap
使用Entry
,因此SparseArray
比HashMap
消耗的内存要少得多,请参阅this
【讨论】:
【参考方案7】:很遗憾编译器会发出警告。我猜 HashMap 已经被过度用于存储项目了。
SparseArrays 有自己的位置。鉴于他们使用二进制搜索算法在数组中查找值,您必须考虑您在做什么。二分查找是 O(log n),而哈希查找是 O(1)。这并不一定意味着对于给定的数据集,二分查找速度较慢。但是,随着条目数量的增加,哈希表的功能开始发挥作用。因此,条目数较少的 cmets 可以等于并且可能比使用 HashMap 更好。
HashMap 仅与散列一样好,并且还会受到负载因子的影响(我认为在以后的版本中它们会忽略负载因子,因此可以更好地优化)。他们还添加了一个二级哈希以确保哈希是好的。这也是 SparseArray 对于相对较少的条目(
我建议如果你需要一个哈希表并且想要更好地使用原始整数(没有自动装箱)等的内存,试试 trove。 (http://trove.starlight-systems.com - LGPL 许可证)。 (与 trove 无关,就像他们的图书馆一样)
通过简化的多 dex 构建,您甚至无需重新打包 trove 即可满足您的需求。 (trove 有很多类)
【讨论】:
以上是关于SparseArray 与 HashMap的主要内容,如果未能解决你的问题,请参考以下文章