SparseArray 与 HashMap

Posted

技术标签:

【中文标题】SparseArray 与 HashMap【英文标题】:SparseArray vs HashMap 【发布时间】:2014-10-23 00:20:16 【问题描述】:

我可以想到几个原因,为什么带有整数键的HashMaps 比SparseArrays 好得多:

    SparseArrayandroid 文档说“它通常比传统的 HashMap 慢”。 如果您使用HashMaps 而不是SparseArrays 编写代码,您的代码将适用于地图的其他实现,并且您将能够使用为地图设计的所有Java API。 如果您使用HashMaps 而不是SparseArrays 编写代码,您的代码将在非android 项目中工作。 地图会覆盖equals()hashCode(),而SparseArray 不会。

然而,每当我尝试在 Android 项目中使用带有整数键的 HashMap 时,IntelliJ 都会告诉我应该改用 SparseArray。我觉得这真的很难理解。有人知道使用SparseArrays 的任何令人信服的理由吗?

【问题讨论】:

【参考方案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 

在内存方面,这里是 SparseIntArrayHashMap&lt;Integer, Integer&gt; 的 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)

迭代项目

您可以使用keyAtvalueAt 一些索引来循环遍历集合,因为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不能让 SparseArray 实现 Map 以便您获得内存高效的 Map 实现- 两全其美。 @PaulBoddington 还记得SparseArray 防止关键整数是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,因此SparseArrayHashMap 消耗的内存要少得多,请参阅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的主要内容,如果未能解决你的问题,请参考以下文章

SparseArray源码分析

SparseArray源码分析

SparseArray源码分析

SparseArray具体解释,我说SparseArray,你说要!

性能优化SparseArray

Android SparseArray源码分析