Kotlin | 从SparseArray、ArrayMap的set操作符看类型检查的不同

在Kotlin开发中,在SparseArrayArrayMap上使用相同的set操作符时,发现类型检查行为还不太一致。

示例

kotlin 复制代码
class MapDemo {
    private val mArrayMap = ArrayMap<Int, Boolean>()
    private val mSparseArray = SparseArrayCompat<Boolean>()

    fun test() {
        mArrayMap.set(4, null) //允许直接设置为null, ✅ 编译通过
        mArrayMap[3] = null //同set方法, ✅ 编译通过
        mArrayMap.get(1) //没有对应的key,直接返回null

        mSparseArray.set(3, null) //不允许直接设置为null, ❌ 编译错误
        mSparseArray[3] = null //同set方法, ❌ 编译错误
        mSparseArray.get(1) //没有对应的key,直接返回null
    }
}

可以看出虽然定义的 ArrayMap<Boolean>,但是依然可以把 value 设置为 null;而SparseArrayCompat<Boolean>不允许直接设置为null,会在编译阶段就报错。两者的get()方法表现一致,如果没有对应的key,都会返回默认值null。

ArrayMap 对应源码

ArrayMap 设计初衷是为了在小数据量场景下,比传统的 HashMap 更节省内存。它通过两个数组来模拟 Map 的功能。

ini 复制代码
//ArrayMap.java
public class ArrayMap<K, V> extends SimpleArrayMap<K, V> implements Map<K, V> {

   int[] mHashes;
   Object[] mArray;
   int mSize;
   
   //通过key获取value,如果没有对应的key,直接返回null
   public V get(Object key) {
      return getOrDefault(key, null);
   }

   public V getOrDefault(Object key, V defaultValue) {
        final int index = indexOfKey(key);
        return index >= 0 ? (V) mArray[(index << 1) + 1] : defaultValue;
   }

    //通过双数组结构高效插入,在保证性能的同时大幅减少内存开销,核心流程:查找 → 更新/插入 → 扩容 → 返回值
    @Nullable
    @SuppressWarnings("unchecked")
    public V put(K key, V value) {
        final int osize = mSize;
        final int hash;
        int index;
        if (key == null) { //处理null键
            hash = 0;
            index = indexOfNull();
        } else { //二分查找
            hash = key.hashCode();
            index = indexOf(key, hash);
        }
        //计算值在数组中的位置,获取旧值并更新新值
        if (index >= 0) {
            index = (index<<1) + 1;
            final V old = (V)mArray[index];
            mArray[index] = value;
            return old;
        }

        index = ~index;
        //动态扩容
        if (osize >= mHashes.length) {
            final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
                    : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            //分配新数组并拷贝数据
            allocArrays(n);

            if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
                throw new ConcurrentModificationException();
            }

            if (mHashes.length > 0) {
                System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
                System.arraycopy(oarray, 0, mArray, 0, oarray.length);
            }

            freeArrays(ohashes, oarray, osize);
        }
        
        if (index < osize) {
            System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
            System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
        }

        if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
            if (osize != mSize || index >= mHashes.length) {
                throw new ConcurrentModificationException();
            }
        }

        mHashes[index] = hash;
        mArray[index<<1] = key;
        mArray[(index<<1)+1] = value;
        mSize++;
        return null;
    }
}

//set是个扩展函数
@kotlin.internal.InlineOnly
public inline operator fun <K, V> MutableMap<K, V>.set(key: K, value: V): Unit {
    put(key, value)
}

ArrayMap实现了Map接口,继承其操作符。Kotlin将ArrayMap视为"平台类型",在类型检查上采取宽容态度,允许潜在的null值传递。

ArrayMap vs HashMap

ArrayMap的数据结构: ArrayMap使用两个数组实现:mHashes数组按升序存储所有key的哈希值,用于快速二分查找;mArray数组以key0, value0, key1, value1...的交替顺序存储实际的键值对。通过这种设计,查找时先在mHashes中二分定位位置索引,再根据索引映射到mArray中获取对应的键值,在保证小数据量下O(log n)查找性能的同时,大幅减少了内存开销,避免了HashMap中Node对象的创建成本。

HashMap的数据结构: 单个Node结构:

HashMap采用数组+链表+红黑树的结构(JDK1.8引入):一个Node数组作为哈希桶,通过hash(key) & (n-1)计算桶索引;发生哈希冲突时,使用链表法在同一个桶内形成链表;当链表长度超过8且数组容量达到64时,链表自动转换为红黑树以维持O(1)级别的查找性能。这种设计在应对哈希碰撞和大量数据时能保持高效操作,但每个键值对都需要封装为Node对象,内存开销较大。

ArrayMap优点 (相对于 HashMap)

  • 内存效率高: HashMap 需要为每个 Entry 创建一个 HashMap.Node 对象(包含 key, value, hash, next 等字段),对于小数据量来说,对象开销大。 ArrayMap 使用两个数组,避免了大量小对象的创建,内存布局更紧凑,尤其在小数据量(<1000 个元素)时优势明显。
  • 缓存友好: 数组结构在内存中是连续的,遍历时缓存命中率更高。

缺点 (相对于 HashMap)

  • 插入和删除性能差: 因为涉及数组元素的移动,时间复杂度为 O(n)。而 HashMap 在理想情况下是 O(1)。
  • 查找性能稍差: 虽然二分查找是 O(log n),但在数据量非常小(个位数)时,可能与 HashMap 的 O(1) 差不多,但随着数据量增大,效率会低于 HashMap。
  • 不适合大数据量: 当数据量很大时(例如超过 1000),数组扩容和元素移动的成本会变得非常高,性能会显著下降。

SparseArrayCompat对应源码

SparseArray的数据结构: SparseArray 使用两个并行数组:mKeys数组按升序存储int类型的键,mValues数组存储对应的Object值。通过有序的键数组实现二分查找,在键为int类型的场景下避免了自动装箱开销,比HashMap节省内存。

ini 复制代码
public class SparseArrayCompat<E> implements Cloneable {

    public void put(int key, E value) {
        int i =  ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            mValues[i] = value;
        } else {
            i = ~i;

            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }

            if (mGarbage && mSize >= mKeys.length) {
                gc();

                // Search again because indices may have changed.
                i = ~ ContainerHelpers.binarySearch(mKeys, mSize, key);
            }

            if (mSize >= mKeys.length) {
                int n =  ContainerHelpers.idealIntArraySize(mSize + 1);

                int[] nkeys = new int[n];
                Object[] nvalues = new Object[n];

                // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
                System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
                System.arraycopy(mValues, 0, nvalues, 0, mValues.length);

                mKeys = nkeys;
                mValues = nvalues;
            }

            if (mSize - i != 0) {
                // Log.e("SparseArray", "move " + (mSize - i));
                System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
                System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
            }

            mKeys[i] = key;
            mValues[i] = value;
            mSize++;
        }
    }

    @Nullable
    public E get(int key) {
        return get(key, null);
    }

    public E get(int key, E valueIfKeyNotFound) {
          int i =  ContainerHelpers.binarySearch(mKeys, mSize, key);

          if (i < 0 || mValues[i] == DELETED) {
              return valueIfKeyNotFound;
          } else {
              return (E) mValues[i];
          }
       }
    }

//set扩展函数
inline operator fun <T> SparseArrayCompat<T>.set(key: Int, value: T) = put(key, value)

set方法中,泛型参数<T>直接传递给put方法,保持了原始泛型参数的协变关系,从而编译器能够进行完整的类型推断和检查。所以当声明SparseArrayCompat<Boolean>时,set操作符的value参数类型被严格推断为Boolean(非空),因此拒绝null值。

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui