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值。

相关推荐
_小马快跑_1 小时前
Android | 为什么有了ArrayMap还要再设计SparseArray?
android
_小马快跑_2 小时前
Android TextView图标对齐优化:使用LayerList精准控制drawable位置
android
_小马快跑_2 小时前
Kotlin协程并发控制:多线程环境下的顺序执行
android
_小马快跑_2 小时前
Kotlin协程异常捕获陷阱:try-catch捕获异常失败了?
android
_小马快跑_2 小时前
Android | 权限申请与前置说明弹窗同时展示的优雅方案
android
_小马快跑_2 小时前
Android | Channel 与 Flow的异同点
android
_小马快跑_2 小时前
Android | 文本测量:从 Paint.measureText 到 StaticLayout 的替换
android
树獭非懒3 小时前
告别繁琐多端开发:DivKit 带你玩转 Server-Driven UI!
android·前端·人工智能
三少爷的鞋4 小时前
为什么应该先在 IntelliJ 中学习 Kotlin 与协程,而不是直接上 Android Studio
android