4. JNI访问Java对象字段
字段访问的核心:先拿 jfieldID,再按类型读写
JNI 访问 Java 实例字段分两步:先用 GetFieldID 从对象的类中取得字段 ID(jfieldID),再用类型化的 Get<Type>Field / Set<Type>Field 读写。字段 ID 与具体对象实例无关,只与「类 + 字段名 + 字段签名」绑定,因此可以缓存复用。
字段签名描述的是字段的 JVM 类型描述符,而不是 Java 源码写法。基本类型有单字符签名,引用类型用 L全限定名;(点换成斜杠,结尾带分号)。
| Java 字段类型 | 字段签名 | 读取函数 | 写入函数 |
|---|---|---|---|
int | I | GetIntField | SetIntField |
long | J | GetLongField | SetLongField |
boolean | Z | GetBooleanField | SetBooleanField |
double | D | GetDoubleField | SetDoubleField |
String | Ljava/lang/String; | GetObjectField | SetObjectField |
完整实现:读写 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 侧提供访问器。