Android 栅格数据缓冲区的类型包装:用匿名子类破解泛型擦除

泛型在运行时被擦除,那就用匿名子类把类型信息"焊"回去------一个字节缓冲按任意类型读写。

前言

处理栅格数据(影像、高程网格、DEM)时,一个绕不开的需求是:同一块连续内存,希望它能按不同数据类型来解释。有时里面是 8 位灰度,有时是 16 位整型高程,有时又是 32 位浮点。如果你为每种类型各写一套类,代码爆炸;如果你用 Object 硬扛,又慢又难维护。

这个坑很磨人:你明明知道这块缓冲是 Int,却只能在运行时靠一堆 if/else 强转;想写一个通用的 get(),编译器不让你按 T 去读内存,因为运行时的 T 已经没了。泛型带来的类型安全,在读取那一刻反而成了障碍。

这套引擎的栅格缓冲区就用"匿名子类"把类型信息在构造时固化下来。下面讲一个很实用的技巧:利用匿名子类,让一个字节缓冲可以零开销地按任意数据类型读写。

一、问题长什么样

假设你有一个通用的数据缓冲抽象,希望它能封装任意类型的栅格值:

kotlin 复制代码
abstract class DataBuffer<T> {
    protected val buffer: ByteBuffer
    abstract fun get(): T      // 按 T 的类型读当前值
    fun put(value: Any): DataBuffer<T>   // 写入任意原始类型
}

你面对的核心矛盾是:get() 里到底调用 getByte()getShort()getInt() 还是 getDouble()?这取决于 T 是什么,但运行时的 T 是未知的 (被擦除了)。你总不能在泛型方法里写 when(T::class),因为 T::class 取不到。

更麻烦的是索引语义。栅格是二维数组,但内存是线性的。一个"位置 i"应该是"第 i 个元素",而不是"第 i 个字节"------对 INT 类型,第 10 个整数对应第 40 个字节。

结论先行:只要涉及"按泛型类型读内存",类型擦除就一定会在运行时挡路。

二、为什么"擦除"让人抓狂

Java/Kotlin 的泛型是编译期类型检查、运行时擦除 的。也就是说,DataBuffer<Int>DataBuffer<Double> 在运行时刻是同一个类,T 只是一个编译期符号,运行时不存在。所以:

  1. 你没法用 T 直接索引到正确的读法;
  2. 一个"读当前值"的抽象方法,必须在具体类型下实现;
  3. 但你又不想为 7 种类型各写 7 个完整子类。

这就是"类型擦除 + 需要按类型读内存"这两件事的天然冲突。

运行时的 T 不存在,所以"按 T 读内存"这件事只能下沉到具体类型。

2.1 擦除带来的索引歧义

同样的 get(i),对 Byte 是第 i 个字节、对 Int 是第 4i 个字节。下标语义依赖类型,而类型运行时没了,基类自己算不出正确的字节偏移。

2.2 不想写 7 个完整子类

栅格类型一共就那么几种,每种只需覆盖一个 get()。为每个类型都写一整套完整子类,重复逻辑(索引换算、重采样、flip)会被复制 7 遍,维护成本陡增。

三、解法:匿名子类在构造时把类型"焊"回去

思路很直接:既然 T 在运行时不存在,那就让每个实例在构造时就"自带"一份正确的读取行为。 用匿名子类,每种 DataType 生成一个只覆盖 get() 的匿名实例:

kotlin 复制代码
companion object {
    fun create(buffer: ByteBuffer, datatype: DataType): DataBuffer<*> =
        when (datatype) {
            DataType.BYTE -> object : DataBuffer<Byte>(buffer, datatype) {
                override fun get() = buffer.get()
            }
            DataType.SHORT -> object : DataBuffer<Short>(buffer, datatype) {
                override fun get() = buffer.getShort()
            }
            DataType.INT -> object : DataBuffer<Int>(buffer, datatype) {
                override fun get() = buffer.getInt()
            }
            DataType.FLOAT -> object : DataBuffer<Float>(buffer, datatype) {
                override fun get() = buffer.getFloat()
            }
            DataType.DOUBLE -> object : DataBuffer<Double>(buffer, datatype) {
                override fun get() = buffer.getDouble()
            }
            // ... 其余类型类似
        }
}

每个匿名子类只覆盖一行 get(),其余逻辑(索引换算、resample、flip)全在基类里共用。这既避免了写 7 个完整子类的爆炸,又把"类型信息"以多态 的形式固化到了每个实例里------get() 被调用时,虚拟分派自动走到正确的读取代码,运行时开销为零。

位置换算则由基类统一处理:把"第 i 个元素"换算成"第 i × 类型字节数 个字节":

kotlin 复制代码
fun get(i: Int): T {
    buffer.position(i * datatype.size())   // datatype.size() = 字节数
    return get()
}

跨类型写入 ,用 instanceof 分派即可,因为写入侧拿到的是明确的具体值:

kotlin 复制代码
fun put(value: Any): DataBuffer<T> {
    when (value) {
        is Byte -> buffer.put(value)
        is Short -> buffer.putShort(value)
        is Int -> buffer.putInt(value)
        is Double -> buffer.putDouble(value)
        else -> throw IllegalArgumentException("unknown value: $value")
    }
    return this
}

重采样同样可以抽象在基类:最近邻插值,本质就是"从源缓冲按比例取像素填到目标缓冲":

kotlin 复制代码
fun resample(from: Dimension, to: Dimension): DataBuffer<T> {
    val out = create(to.width * to.height, datatype)
    val xRatio = from.width.toDouble() / to.width
    val yRatio = from.height.toDouble() / to.height
    for (y in 0 until to.height) {
        for (x in 0 until to.width) {
            val px = floor(x * xRatio).toInt()
            val py = floor(y * yRatio).toInt()
            out.put(y * to.width + x, get(py * from.width + px))
        }
    }
    return out
}

整条链路下来,调用方永远面对一个统一的 DataBuffer<T>,内部却按真实类型在读写------类型抽象的正确性,靠的是多态而非 Object 强转。

匿名子类把类型信息固化进实例,多态分派零开销地解决了擦除问题。

3.1 位置换算统一收口到基类

"第 i 个元素 → 第 i×size 个字节"只有一处实现,所有类型共用,避免每个子类各算一遍偏移而出错。

3.2 写入侧用 instanceof 分派

写入时拿到的是具体值,类型已知,用 when (value) 匹配 is Byte/Short/... 即可正确落字节,比读取侧更直白。

四、升华:什么时候用这套,什么时候该换

"匿名子类固化类型"这套手法,适合类型集合固定、且每种类型只需覆盖很少几个方法 的场景。栅格数据类型就那么 7 种,每种只需覆盖一个 get(),用匿名子类最划算。

但它的局限也很明显:如果子类方法很多、或者类型可能动态扩展,匿名子类代码会膨胀。替代方案有:

  • 类型化常量 :把"读取函数"存成函数对象 (ByteBuffer) -> T,用一张 Map<DataType, 读取函数> 查表,比一串匿名子类更扁平、更易测。
  • 无类型中间表示 :统一读成 Double,牺牲一点精度换极简(适合精度要求不高的可视化)。
  • 序列化框架:若与存储强绑定,直接用现成的栅格库(如 GeoTIFF 解析库)更省心。

选型的核心判断是:类型种类是否有限且稳定。 有限稳定,就用固化类型;频繁扩展,就上函数对象或查表。

类型有限且稳定才用匿名子类;频繁扩展应换函数对象查表。

五、结论

  • 泛型在运行时被擦除,导致 DataBuffer<T> 无法按 T 直接读内存。
  • 匿名子类 在构造时为每种数据类型固化一个只覆盖 get() 的实例,运行时零开销。
  • 位置换算(第 i 个元素 → 第 i×size 个字节)统一放在基类,跨类型写入用 instanceof 分派。
  • 重采样、flip、rewind 等通用逻辑全部抽象在基类,子类只负责"按类型读一个值"。
  • "类型集合有限稳定"是使用此手法的前提;类型多变时应改用函数对象查表等更扁平的结构。

你在处理栅格或二进制缓冲时,被泛型擦除坑过吗?欢迎在评论区聊聊你是怎么把类型信息留到运行时的。

相关推荐
叶羽西1 小时前
Android Camera HAL调整图像处理线程优先级
android
HouWan2 小时前
Flutter: MediaQuery.of(context) 为什么可能拖慢页面?
android·flutter·ios
事圆则缓2 小时前
Android 图片内存到底怎么算
android
Godikov2 小时前
弱网环境离线优先:Android 终端数据可靠同步架构实战
android
爱编程的小新☆5 小时前
Fake GPS 虚拟定位保姆级使用教学
android·移动开发·gps·虚拟定位
执明wa6 小时前
Android RecyclerView 多类型, 多种 Item
android·xml·开发语言·设计模式·android studio
silianpan7 小时前
CAD 文档预览 UTS 插件
android·ios·harmonyos
小麦在野7 小时前
独立 App 开发系列:用 GitHub Pages 托管隐私政策和用户协议
android·github