intellij老是警告的SparseArray是什么 - HashMap的替代者

本文对比分析了SparseArray与HashMap在内存占用及性能上的差异,通过源码解析和实验测试,得出SparseArray更适合移动端整数键的应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如果只想看比较和结论,可以直接跳到最后

序言

身为一个有代码洁癖的程序员,在写Android应用的时候,我总是会去注意

  • 代码规范(Google Android Guideline)
  • 能一行搞定的代码,绝不写两行
  • 决不让编译器(intellij, as)右边滚动条有黄色
  • 不重复自己

当然了,实际开发中,编译器报的warning有些不太好避免,比如有些空指针,编译器从android源码来看,觉得不会出现空指针,但是实际情况下….你懂得,部分rom手贱改坏了源码,结果就crash了,所以我们能做的,就是尽量减少warning。

扯了这么多,说回主题,有时候在HashMap申明那行,intellij会报warning,说用SparseArray更好,那么SparseArray究竟是什么东西,为什么更好,为什么提示说更省内存呢?

本文以api 21的源码为准

SparseArray源码分析

// E对应HashMap的Value
public class SparseArray<E> implements Cloneable {
    // 用来优化删除性能,标记已经删除的对象
    private static final Object DELETED = new Object();
    // 用来优化删除性能,标记是否需要垃圾回收
    private boolean mGarbage = false;

    // 存储索引,整数索引从小到大被映射在该数组
    private int[] mKeys;
    // 存储对象
    private Object[] mValues;
    // 实际大小
    private int mSize;

构造函数:

    public SparseArray(int initialCapacity) {
        if (initialCapacity == 0) {
            // EmptyArray是一个不需要数组分配的轻量级表示。
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        mSize = 0;
    }

这里也充分节约了内存。newUnpaddedObjectArray最后指向了VMRuntime的一个native方法

/**
 * 返回一个至少长minLength的数组,但可能更大。增长的大小来自于避免数组后的任何padding。padding的大小依赖于componentType和内存分配器的实现
 */
public native Object newUnpaddedArray(Class<?> componentType, int minLength);

Get方法使用了二分查找

/**
 * 获得指定key的映射对象,或者null如果没有该映射。
 */
public E get(int key) {
    return get(key, null);
}

@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
    // 二分查找
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    // 如果没找到或者该value已经被标记删除
    if (i < 0 || mValues[i] == DELETED) {
        return valueIfKeyNotFound;
    } else {
        return (E) mValues[i];
    }
}

对应的二分查找实现这里就不赘述了,大致就是一个对有序数组一分为二比较中间值和目标值的循环查找。

其中比较巧妙的一点是在没有找到的时候会返回一个~low的index,其有两个作用:

  • 告诉调用者没有找到
  • 调用者可以直接用~result获得该元素应该插入的位置(-(insertion point) - 1)。

对应的put方法

/**
 * 添加一个指定key到指定object的映射,如果之前有一个指定key的映射则直接替换掉原映射object。
 */
public void put(int key, E value) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        // 该key原来有了,替换掉
        mValues[i] = value;
    } else {
        // 做一个负运算,获得应该插入的index
        i = ~i;

        // size足够且原value已经被标记为删除
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
        // 走到这里就说么i超出了size,或者对应元素是有效的

        // 被标记为需要垃圾回收且SparseArray大小不小于keys数组长度
        if (mGarbage && mSize >= mKeys.length) {
            // 压缩空间(这里源码有点逗,竟然还有Log.e的注释留在那里,看来Android源码工程师也是要调试的),会压缩数组,把无效的值都去掉,保证连续有效值
            gc();

            // 再次查找因为索引可能改变
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }

        // 插入,如果size不够则会重新分配更大的数组,然后拷贝过去并插入;size足够则用System.arraycopy把插入位置开始的value都后移然后插入
        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        // 实际大小加1
        mSize++;
    }
}

在看看remove(del)方法

/**
 * 如果有的话,删除对应key的映射
 */
public void delete(int key) {
    // 又是二分查找
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    // 存在则标记对应value为DELETED,且置位mGarbage
    if (i >= 0) {
        if (mValues[i] != DELETED) {
            mValues[i] = DELETED;
            mGarbage = true;
        }
    }
}

/**
 * {@link #delete(int)}的别名.
 */
public void remove(int key) {
    delete(key);
}

/**
 * 删除指定索引的映射(这个有点暴力啊,用的应该比较少吧,直接指定位置了)
 */
public void removeAt(int index) {
    // 就是delete里面那段方法,如此说来为什么delete不调用removeAt而要重复这段代码呢
    if (mValues[index] != DELETED) {
        mValues[index] = DELETED;
        mGarbage = true;
    }
}

大致看了crud的这几个方法,应该有个一个初步了解了吧,SparseArray因为key是一个纯粹的整数数组,避免了auto-boxing key和额外的数据结构去映射K/V关系,从而节省了内存。

当然了,这里也有一个tradeoff,由于key数组需要有序,所以每次都会相对简单的写操作更花费时间,要去二分查找,要在数组删除/插入元素。所以对应地为了优化,因为了mGarbage和DELETED,将可能的多次gc合并为一次,延迟到必须的时候执行。

源码注释里也提到了,该类不适合用于大数据量,上百个entry的时候差别在50%以内,尚可以接受,毕竟在移动端我们缺的,往往是内存,而不是CPU。

HashMap源码分析

相较SparseArray,HashMap的实现就复杂一点了,因为其支持多种key(甚至null),还要实现Iterator。这里我们主要看看基本操作实现。

/**
 * transient关键字表示不需要序列化。
 * Hash表,null key的在下面。
 * HashMapEntry定义了K/V映射,hash值,以及next元素
 */
transient HashMapEntry<K, V>[] table;

/**
 * 该entry代表null key, 或者不存在该映射.
 */
transient HashMapEntry<K, V> entryForNullKey;

/**
 * hash map的映射数量.
 */
transient int size;

/**
 * 结构修改的时候自增,来做(最大努力的)并发修改检测
 */
transient int modCount;

/**
 * hash表会在大小超过该阙值的时候做rehash。通常该值为0.75 * capacity, 除非容量为0,即上面的EMPTY_TABLE声明。
 */
private transient int threshold;

public HashMap(int capacity) {
    if (capacity < 0) {
        throw new IllegalArgumentException("Capacity: " + capacity);
    }

    if (capacity == 0) {
        // 和SparseArray类似,所有空的实例共享同一个EMPTY表示
        HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE;
        table = tab;
        // 强制先put()来替换EMPTY_TABLE
        threshold = -1;
        return;
    }

    if (capacity < MINIMUM_CAPACITY) {
        capacity = MINIMUM_CAPACITY;
    } else if (capacity > MAXIMUM_CAPACITY) {
        capacity = MAXIMUM_CAPACITY;
    } else {
        // 难道是为了内存padding
        capacity = Collections.roundUpToPowerOfTwo(capacity);
    }
    makeTable(capacity);
}

/**
 * 根据给定容量分配一个hash表,并设置对应阙值。
 * @param newCapacity 必须是2的次数,因为要内存对齐
 */
private HashMapEntry<K, V>[] makeTable(int newCapacity) {
// 这么做难道是为了同步性考虑
HashMapEntry<K, V>[] newTable
            = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
    table = newTable;
    threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
    return newTable;
}

然后是插入的代码

/**
 * 映射指定key到指定value,如果有原对应mapping返回其value,否则返回null。
 */
@Override public V put(K key, V value) {
    if (key == null) {
        // key为空直接去putValueForNullKey方法
        return putValueForNullKey(value);
    }
    // 计算该key的hash值,根据key本身的hashCode做二次hash
    int hash = Collections.secondaryHash(key);
    HashMapEntry<K, V>[] tab = table;
    int index = hash & (tab.length - 1);
    for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
        // 原来有对应entry了,直接修改value,然后返回oldValue
        if (e.hash == hash && key.equals(e.key)) {
            preModify(e);
            V oldValue = e.value;
            e.value = value;
            return oldValue;
        }
    }

    // 没有现存的entry,创建一个
    modCount++;
    // size超出阙值,double容量,重新计算index
    if (size++ > threshold) {
        tab = doubleCapacity();
        index = hash & (tab.length - 1);
    }
    // 在table的index位置插入一个新的HashMapEntry,其next是自己
    addNewEntry(key, value, hash, index);
    return null;
}

private V putValueForNullKey(V value) {
    HashMapEntry<K, V> entry = entryForNullKey;
    // 和上面类似的逻辑,如果存在则替换,否则创建该HashMapEntry
    if (entry == null) {
        addNewEntryForNullKey(value);
        size++;
        modCount++;
        return null;
    } else {
        preModify(entry);
        V oldValue = entry.value;
        entry.value = value;
        return oldValue;
    }
}

姑且说到这里,大致可以清楚,HashMap是一个以hash为中心的实现,在size上,也只有double的逻辑,而没有remove后是否缩小capacity的逻辑。时间复杂度O(1)的代价就是耗费大量内存来存储数据。

比较

HashMap -> 快,浪费内存
SparseArray -> 会有性能损耗, 节约内存

我们做一个简单的性能测试,不带参数初始化HashMap和SparseArray,同样存储Integer->String的映射

用:符号来分割HashMap和SparseArray的耗时(毫秒)

实验第一次第二次第三次第四次
依次put 10000个数13:712:676:414:6
随机put 10000个数16:8318:8515:7617:78

4次测试结果相近(有一个噪音数据76/4有待研究),HashMap在有序的key时候,更耗时,而在无需key的时候则是SparseArray更耗时,而HashMap则没有太大的性能差别。

再对随机put后的结果做了10000次get后,获得了7:3,7:3,8:3的结果,可见get操作上,SparseArray性能更好,但即便在10000个entry的时候,差别其实也并不大。

在内存上,看到SparseArray在Allocation Tracker中为32,而HashMap则达到了69632这个可怕的数字。。。。。。

结论

在key为整数的情况下,考虑到移动端往往K/V对不会太大,所以用SparseArray能更节省内存,且性能损耗在可接受的范围。

从试验结果看,SparseArray相较于HashMap,大大节省了内存,对移动端实在是不二的选择。

<think>嗯,我现在是一个Java开发者,想学Python。应该从哪里开始呢?首先,我需要比较一下Java和Python的区别,这样学起来可能更容易。Java是静态类型,Python是动态类型,这意味着变量类型在Python中不需要声明,可能会让代码更简洁,但也容易出错?比如在Java里,我得写int a = 5;,而Python直接a = 5。这可能一开始不太习惯,但应该没问题。 然后是语法方面。Java的语法比较繁琐,比如类定义、大括号等等,而Python用缩进来区分代码块,没有大括号,这样代码看起来更简洁。不过缩进要是错了,程序就会出错,这点需要注意。之前用Java的时候,有时候缩进只是为了可读性,但在Python里这是必须的,得小心处理。 接下来是学习资源。用户提到官方文档和教程,比如Python官网的教程,还有《Python Crash Course》和《Automate the Boring Stuff with Python》这两本书。可能先看看官网的教程,因为是免费的,而且比较权威。另外,像Codecademy和Coursera的课程,交互式学习可能更有帮助,可以边学边练。 然后是练习项目。用户建议从小工具开始,比如计算器、待办事项应用,或者用Flask/Django做web应用。我之前做过Java的Web项目,可能用Django会比较合适,因为它的结构和Spring有点像?或者Flask更轻量级,适合快速开发。另外数据分析方面,比如用Pandas处理数据,或者爬虫,用Requests和BeautifulSoup,这些可能和Java的生态不同,需要学习新的库。 集成开发环境的话,PyCharm可能和IntelliJ类似,毕竟都是JetBrains的产品,应该用起来顺手。VS Code的话轻量级,插件多,可能也不错。Jupyter Notebook适合数据分析,可以边写代码边看结果,这对调试和可视化有帮助。 关于思维转换,用户提到动态类型、解释型语言、简洁的语法。比如在Java里需要设计接口和类,Python可能更灵活,可以用鸭子类型,或者直接写函数。比如Python的列表和字典比Java的集合框架更简洁,内置了很多方法。还有Python的异常处理,可能和Java类似,但语法不同,比如try-except而不是try-catch。 常见库方面,Web开发有Django、Flask,数据分析有NumPy、Pandas,机器学习有TensorFlow、PyTorch,爬虫有Requests、Scrapy。这些库可能需要花时间逐个学习,特别是如果我要转数据分析或机器学习方向的话,得深入学这些库。 最后是持续实践,参与开源项目或者自己做一些小项目。比如用Python自动化日常工作,处理Excel文件,或者写脚本备份文件,这些实际应用能巩固知识。可能还可以参加一些在线编程挑战,比如LeetCode用Python解题,这样同时锻炼算法和Python语法。 有没有其他需要注意的地方呢?比如Python的版本问题,现在应该用Python3,因为Python2已经停止支持了。虚拟环境的使用,比如venv或者conda,管理项目依赖。还有Python的包管理工具pip,和Java的Maven/Gradle类似,但用法不同。另外,Python的多线程和Java有所不同,因为GIL的存在,多线程在CPU密集型任务上可能不如Java高效,可能需要用多进程或者异步编程。 可能刚开始的时候,会不自觉地用Java的思维写Python代码,比如过度设计类结构,而Python更倾向于简洁和利用内置结构。比如在Java里可能会用getter和setter,而Python可以直接访问属性,或者用property装饰器。还有Python的装饰器语法,可能需要时间适应,但这也是Python强大的一部分。 总之,学习路径应该是先熟悉基本语法,然后通过项目实践,同时学习常用库,逐步转换思维方式。遇到问题多查文档,参与社区讨论,比如Stack Overflow或者Reddit的Python板块。可能还需要关注Python的最佳实践,比如PEP8代码风格,保持代码的可读性。</think>学习Python作为Java开发者是一个很好的选择,以下是为您量身定制的学习路径和建议: --- ### **1. 理解Python与Java的核心差异** - **动态类型 vs 静态类型** Python变量无需声明类型,类型在运行时确定。例如:`a = 5`(Python) vs `int a = 5;`(Java)。注意动态类型的灵活性可能带来调试挑战。 - **语法简洁性** Python用缩进替代大括号,代码更简洁。例如: ```python if a > 0: print("Positive") else: print("Non-positive") ``` - **解释型语言** Python无需编译,直接运行(如`python script.py`),适合快速迭代。 --- ### **2. 快速入门资源** - **官方教程** 从[Python官方文档](https://docs.python.org/3/tutorial/)开始,掌握基础语法。 - **书籍推荐** - 《Python Crash Course》:项目驱动,适合实践。 - 《Automate the Boring Stuff with Python》:学习用Python自动化日常任务。 - **在线课程** - [Codecademy的Python课程](https://www.codecademy.com/learn/learn-python-3)(交互式学习)。 - Coursera的[Python for Everybody](https://www.coursera.org/specializations/python)(适合零基础)。 --- ### **3. 关键学习重点** - **基础语法** 快速掌握列表、字典、集合、生成器、装饰器等核心语法。 - **常用库** - **Web开发**:Django(类似Java的Spring)或Flask(轻量级)。 - **数据分析**:Pandas(类似Excel,处理结构化数据)、NumPy(数值计算)。 - **爬虫**:Requests(HTTP库)+ BeautifulSoup/Scrapy(解析数据)。 - **机器学习**:Scikit-learn、TensorFlow/PyTorch。 - **工具链** - **IDE**:PyCharm(Java开发者熟悉JetBrains风格)或VS Code(轻量级)。 - **Jupyter Notebook**:交互式数据分析。 - **虚拟环境**:用`venv`或`conda`管理依赖。 --- ### **4. 实践项目建议** - **从小工具开始** - 命令行计算器、待办事项管理工具。 - 自动化脚本(批量重命名文件、处理Excel/PDF)。 - **Web应用** - 用Flask搭建REST API(对比Java的Spring Boot)。 - 用Django开发博客系统(学习ORM和MVC模式)。 - **数据分析** - 分析公开数据集(如Kaggle),用Pandas清洗数据并可视化。 - **参与开源** 贡献Python开源项目(如GitHub上的“Good First Issue”标签项目)。 --- ### **5. 思维转换技巧** - **利用Python的简洁性** - 用列表推导替代循环:`[x*2 for x in range(10)]`。 - 使用字典处理键值对,而非Java的`HashMap`。 - **函数优先** Python支持函数式编程(如`map()`、`filter()`),优先用函数而非类。 - **异常处理** Python用`try...except`替代`try...catch`,更灵活的错误捕获。 --- ### **6. 避坑指南** - **版本选择** 使用Python 3(推荐3.8+),避免Python 2的兼容问题。 - **虚拟环境** 为每个项目创建独立环境(`python -m venv myenv`),避免依赖冲突。 - **代码风格** 遵循PEP8规范(用工具如`flake8`或`black`自动格式化)。 --- ### **7. 学习路线总结** 1. **基础语法** → 2. **核心库(如Requests/Pandas)** → 3. **项目实战** → 4. **深入领域(如Web/ML)** --- 通过以上步骤,您能高效过渡到Python开发,同时结合Java经验,快速成为全栈或多领域开发者。遇到问题时,善用[Stack Overflow](https://stackoverflow.com/)和Python社区资源。祝学习顺利!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值