Kotlin @JvmStatic + vararg 在 Java 调用下的 NPE 问题分析与修复

Kotlin @JvmStatic + vararg 在 Java 调用下的 NPE 问题分析与修复

一、问题现象

在稳定性 / 反射拦截模块中,我们封装了一个通用的 getDeclaredMethod,用于统一处理反射调用与拦截逻辑。线上出现 NullPointerException,堆栈定位到 Arrays.copyOf(clsArr, clsArr.length)

Java 侧封装方法:

public static Method getDeclaredMethod(
        Class clazz,
        String name,
        Class<?>... parameterTypes
) throws NoSuchFieldException, NoSuchMethodException {
    return ReflectionInterceptManager.getDeclaredMethod(clazz, name, parameterTypes);
}

Kotlin 侧实现:

@Throws(NoSuchMethodException::class)
@JvmStatic
fun getDeclaredMethod(
    clazz: Class<Any>,
    name: String,
    vararg parameterTypes: Class<*>?
): Method? {
    if (!enable) {
        return clazz.getDeclaredMethod(name, *parameterTypes)
    }
    // ...
}

异常现象:

  • Java 侧部分调用路径直接传入 null
getDeclaredMethod(clazz, name, null);
  • Kotlin 实现中发生 NullPointerException,定位到:
Arrays.copyOf(clsArr, clsArr.length)

二、根因分析(核心)

1) vararg 在 JVM 上的真实形态

无论 Java 还是 Kotlin:

vararg T

在 JVM 上本质就是一个数组参数:

T[]

2) Kotlin 的 vararg Class<*>? 语义被误解

vararg parameterTypes: Class<*>?

真实语义是:

  • parameterTypes 数组本身:不可空
  • parameterTypes[i] 数组元素:可空

Kotlin 无法声明“可空的 vararg 数组”,因此编译器默认数组本身非空。


三、JVM / Kotlin / Java 语义差异

Java 的 vararg 是语法糖

foo(a, b, c)

等价于:

foo(new Object[]{a, b, c})

但 Java 允许:

foo(null);

这在 Java 语义里是合法的:null 可以代表整个数组。


四、为什么字节码是这样

Kotlin 反编译后的形态:

@JvmStatic
@Nullable
public static final Method b(
    @NotNull Class<Object> cls,
    @NotNull String str,
    @NotNull Class<?>... clsArr
) throws NoSuchMethodException {
    return cls.getDeclaredMethod(
        str,
        (Class[]) Arrays.copyOf(clsArr, clsArr.length)
    );
}

关键点:

  • clsArr 被标注为 @NotNull
  • 方法内部没有对 clsArr 做任何 null 检查
  • JVM 层面直接使用 clsArr.length

这是 Kotlin 编译器的必然选择:vararg 在 Kotlin 语义里默认非空。


五、为什么一定会 NPE

调用链复盘:

Java 调用
  ↓
getDeclaredMethod(clazz, name, null)
  ↓
Kotlin @JvmStatic vararg 接收
  ↓
clsArr == null
  ↓
Arrays.copyOf(clsArr, clsArr.length)
  ↓
NullPointerException

结论:问题不在 Kotlin 语义,而在 Java 允许传入 null vararg + Kotlin 无法在字节码层面兜底。


六、修复方案(分层)

方案一:Kotlin 侧兜底(最小侵入,推荐)

@JvmStatic
@Throws(NoSuchMethodException::class)
fun getDeclaredMethod(
    clazz: Class<Any>,
    name: String,
    vararg parameterTypes: Class<*>?
): Method? {
    val safeParams: Array<Class<*>> = if (parameterTypes == null) {
        emptyArray()
    } else {
        parameterTypes.filterNotNull().toTypedArray()
    }
    return clazz.getDeclaredMethod(name, *safeParams)
}

优点:

  • Java 传 null 会被当作“无参数”
  • Kotlin 行为不变
  • 对调用方透明

方案二:API 语义拆分(基础库推荐)

Kotlin 安全版本(不允许 null):

@JvmStatic
fun getDeclaredMethod(
    clazz: Class<Any>,
    name: String,
    vararg parameterTypes: Class<*>
): Method =
    clazz.getDeclaredMethod(name, *parameterTypes)

Java 防御版本(显式数组):

@JvmStatic
fun getDeclaredMethod(
    clazz: Class<Any>,
    name: String,
    parameterTypes: Array<Class<*>>?
): Method =
    clazz.getDeclaredMethod(name, *(parameterTypes ?: emptyArray()))

表达清晰:

  • Java:允许传 null
  • Kotlin:不允许传 null

不推荐方案

方案问题
依赖 @NotNullJava 侧不生效
指望调用方自觉线上一定踩
仅改 Class<*>?无法解决数组为 null

七、最佳实践总结

所有 @JvmStatic + vararg 的 Kotlin API,都必须假设 Java 调用方可能传入 null

尤其在以下场景必须防御:

  • 反射工具
  • Hook / 插桩
  • 稳定性 / Crash 框架
  • 基础工具库

推荐做法:

  1. Kotlin 内部对 vararg 做 null 兜底
  2. 或拆分 Java / Kotlin API
  3. 底层库避免对 Java 暴露“裸 vararg”

八、一句话工程结论

这是一次 Java vararg 与 Kotlin null-safety 在 JVM 层面的必然冲突,
不是代码写错,而是边界没有被显式防御。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值