4. JNI访问Java对象字段

JNI访问Java对象字段 学习卡片

字段访问的核心:先拿 jfieldID,再按类型读写

JNI 访问 Java 实例字段分两步:先用 GetFieldID 从对象的类中取得字段 ID(jfieldID),再用类型化的 Get<Type>Field / Set<Type>Field 读写。字段 ID 与具体对象实例无关,只与「类 + 字段名 + 字段签名」绑定,因此可以缓存复用。

字段签名描述的是字段的 JVM 类型描述符,而不是 Java 源码写法。基本类型有单字符签名,引用类型用 L全限定名;(点换成斜杠,结尾带分号)。

Java 字段类型字段签名读取函数写入函数
intIGetIntFieldSetIntField
longJGetLongFieldSetLongField
booleanZGetBooleanFieldSetBooleanField
doubleDGetDoubleFieldSetDoubleField
StringLjava/lang/String;GetObjectFieldSetObjectField
Java 侧对象 class Counter int value; // I long total; // J String name; // Ljava/lang/String; ① GetFieldID (env, cls, "value", "I") ② 按类型读写 GetIntField(env, obj, fid) SetIntField(env, obj, fid, v) 结果 • fid 可缓存复用 • 读回 Java 字段值 • 写回后 Java 可见 • 签名错 → fid 为 NULL • 需检查 ExceptionCheck 字段签名映射(GetFieldID 第三参数必须精确匹配) int → "I" GetIntField long → "J" GetLongField boolean → "Z" GetBooleanField double → "D" GetDoubleField String → Ljava/lang/String; 引用类型签名规则:L + 全限定名(点换斜杠)+ ; 例如 java.lang.String → Ljava/lang/String; 数组字段:int[] → "[I",String[] → "[Ljava/lang/String;"

完整实现:读写 int 字段并处理签名

下面用一个 Counter 类演示:本地方法读取 value 字段、加一后写回,同时读取 name 引用字段验证签名写法。

// Counter.java
public class Counter {
    public int value = 10;
    public String name = "init";

    public native int bumpAndRead();   // 本地方法:value+1 后返回
    public native String readName();   // 本地方法:返回 name 字段
}

用 javac -h 生成头文件后,实现本地方法。注意 GetFieldID 的第三个参数是字段签名,必须与字段真实类型一致。

// Counter.c
#include <jni.h>
#include <stdio.h>
#include "Counter.h"

JNIEXPORT jint JNICALL Java_Counter_bumpAndRead(JNIEnv *env, jobject obj) {
    // 1. 获取对象的类
    jclass cls = (*env)->GetObjectClass(env, obj);
    if (cls == NULL) return -1;

    // 2. 获取 int 字段 value 的 jfieldID,签名为 "I"
    jfieldID fidValue = (*env)->GetFieldID(env, cls, "value", "I");
    if (fidValue == NULL) {
        // 字段不存在或签名不匹配,JNI 已抛出异常
        return -1;
    }

    // 3. 读取当前值
    jint v = (*env)->GetIntField(env, obj, fidValue);

    // 4. 加一并写回
    (*env)->SetIntField(env, obj, fidValue, v + 1);

    // 5. 读回确认
    jint after = (*env)->GetIntField(env, obj, fidValue);
    printf("JNI: before=%d after=%d\n", v, after);

    (*env)->DeleteLocalRef(env, cls);
    return after;
}

JNIEXPORT jstring JNICALL Java_Counter_readName(JNIEnv *env, jobject obj) {
    jclass cls = (*env)->GetObjectClass(env, obj);
    // 引用类型字段签名:L + 全限定名(斜杠) + ;
    jfieldID fidName = (*env)->GetFieldID(env, cls, "name", "Ljava/lang/String;");
    if (fidName == NULL) return NULL;

    jstring js = (jstring)(*env)->GetObjectField(env, obj, fidName);
    (*env)->DeleteLocalRef(env, cls);
    return js;   // 返回给 Java 侧
}
字段 ID 只依赖「类 + 字段名 + 签名」,与对象实例无关。若同一字段会被反复访问,可在首次调用时把 jfieldID 存入 static 变量缓存,避免每次查找。

验证与常见错误

public class Main {
    public static void main(String[] args) {
        System.loadLibrary("counter");
        Counter c = new Counter();
        System.out.println("Java before: " + c.value);   // 10
        int r = c.bumpAndRead();
        System.out.println("JNI returned: " + r);        // 11
        System.out.println("Java after: " + c.value);    // 11
        System.out.println("name: " + c.readName());     // init
    }
}
  • 签名写错:把 int 写成 "int" 或把 String 写成 "String",GetFieldID 返回 NULL 并抛出 NoSuchFieldError。正确写法是 "I" 和 "Ljava/lang/String;"。
  • 忘记检查 NULL:GetFieldID 失败后继续调用 GetIntField 会导致崩溃。每次取 ID 后都应判空。
  • 用错读写函数:字段是 long 却调用 GetIntField,会读到错误数据。读写函数必须与字段签名类型匹配。
  • 局部引用泄漏:GetObjectClass 返回的 jclass 是局部引用,循环中不释放会耗尽引用表,应及时 DeleteLocalRef。
若字段是 private,GetFieldID 仍能成功获取(JNI 不检查访问修饰符),但这会破坏封装。生产代码应优先通过公开方法访问,或在 Java 侧提供访问器。