在C语言中直接“调用”Java方法是不可能的,因为C和Java是两种完全不同的语言,运行在不同的环境(C在本地操作系统,Java在Java虚拟机JVM)中,它们之间需要一个“桥梁”来进行通信。

这个桥梁就是 Java Native Interface (JNI)。
JNI是Java平台的一部分,它定义了一套规范,允许Java代码与其他语言(主要是C和C++)编写的代码进行交互,通过JNI,你可以:
- 从Java代码调用C/C++本地方法(更常见)。
- 从C/C++代码调用Java方法(你当前的问题)。
下面我将详细解释如何实现 从C语言调用Java方法。
核心思想
整个过程分为两个主要部分:

- Java端:创建一个类,声明一个或多个
native方法,使用javac编译这个类,再用javah(旧版) 或javac -h(新版) 生成一个C语言头文件(.h),这个头文件包含了C函数需要遵循的签名。 - C端:实现头文件中定义的C函数,在这个C函数内部,你会使用JNI提供的API来:
- 找到并加载目标Java类。
- 获取要调用的Java方法的ID。
- 创建Java对象(如果需要)。
- 调用该方法,并传递参数。
- 处理返回值。
详细步骤与示例
假设我们的目标是从C程序中调用Java类的 String sayHello(String name) 方法。
第1步:编写Java代码
我们创建一个Java类,其中包含一个 native 方法。
HelloJNI.java
public class HelloJNI {
// 声明一个native方法,这个方法将由C语言实现
public native String sayHello(String name);
// 加载包含本地库的动态链接库 (.dll on Windows, .so on Linux)
static {
System.loadLibrary("hellojni"); // 注意:这里只写库名,不带lib前缀和.so/.dll后缀
}
public static void main(String[] args) {
// 创建HelloJNI类的实例
HelloJNI helloJNI = new HelloJNI();
// 调用本地方法
String result = helloJNI.sayHello("from C");
System.out.println("Java received from C: " + result);
}
}
第2步:编译Java代码并生成JNI头文件
-
编译Java类:
(图片来源网络,侵删)javac HelloJNI.java
这会生成
HelloJNI.class文件。 -
生成JNI头文件: 使用
javac的-h选项来生成C头文件,这个头文件定义了C函数的签名,C代码必须严格按照这个签名来实现。javac -h . HelloJNI.java
这会在当前目录下生成一个
HelloJNI.h文件。
HelloJNI.h (自动生成)
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
关键点解读 HelloJNI.h:
#include <jni.h>: 包含了所有JNI定义的核心头文件。JNIEXPORT,JNICALL: JNI特定的宏,用于指定函数的调用约定。jstring: JNI中的字符串类型。JNIEnv *: 指向JNI环境的指针,是JNI函数的“总开关”,几乎所有JNI操作都通过它来完成。jobject: 指向Java对象的引用,它指向HelloJNI类的实例。Java_HelloJNI_sayHello: C函数的命名规则是固定的:Java_+ 包名(如果有) +_+ 类名 +_+ 方法名。(JNIEnv *, jobject, jstring): 函数参数列表。Ljava/lang/String;: 这是JNI的“类型签名”,表示java.lang.String类型。(Ljava/lang/String;)Ljava/lang/String;表示sayHello(String): String。
第3步:编写C/C++实现代码
我们来实现 HelloJNI.h 中声明的函数,这个函数将完成从C调用Java sayHello 的核心逻辑。
hellojni.c
#include <stdio.h>
#include "HelloJNI.h" // 包含我们生成的头文件
// JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject, jstring);
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj, jstring name) {
// 1. 将Java String (jstring) 转换为C字符串 (const char*)
const char *c_name = (*env)->GetStringUTFChars(env, name, NULL);
if (c_name == NULL) {
return NULL; // Out of memory
}
printf("C: Received name from Java: %s\n", c_name);
// 2. 在C代码中进行一些处理
char c_message[100];
sprintf(c_message, "Hello, %s! I'm C.", c_name);
// 3. 释放C字符串的内存,避免内存泄漏
(*env)->ReleaseStringUTFChars(env, name, c_name);
// 4. 创建一个新的Java String对象,用于返回
jstring j_message = (*env)->NewStringUTF(env, c_message);
return j_message;
}
代码解释:
JNIEnv *env: 这是JNI环境指针,我们用它来调用JNI函数。(*env)->...: 在C语言中,JNIEnv是一个指向函数指针结构的指针,所以需要这样解引用来调用其内部的函数。GetStringUTFChars: 将JNI的jstring转换为C语言可读的UTF-8格式字符串。ReleaseStringUTFChars: 非常重要! 必须释放GetStringUTFChars分配的资源,否则会导致内存泄漏。NewStringUTF: 创建一个新的jstring对象,从C字符串转换而来,这个对象可以被返回给Java。
第4步:编译C代码为共享库
我们将C代码编译成动态链接库(共享库),Java程序可以通过 System.loadLibrary 来加载它。
在Linux/macOS上:
# -I 指定jni.h的路径,通常是JAVA_HOME/include
# -shared 生成共享库
# -o 指定输出的库名,必须与Java代码中 System.loadLibrary("hellojni") 的名字一致
gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -shared -o libhellojni.so hellojni.c
注意:库名必须是 lib<name>.so 的格式。
在Windows上 (使用MinGW或Visual Studio):
# -I 指定jni.h的路径 # -shared 生成DLL (动态链接库) # -o 指定输出的库名,必须是 <name>.dll 的格式 gcc -I"C:\Program Files\Java\jdk-11.0.12\include" -I"C:\Program Files\Java\jdk-11.0.12\include\win32" -shared -o hellojni.dll hellojni.c
注意:库名必须是 <name>.dll 的格式,与 System.loadLibrary("hellojni") 中的名字一致,不需要 lib 前缀。
第5步:运行Java程序
确保编译好的共享库(.so 或 .dll)位于Java能够找到它的地方,通常有两种方式:
- 放在Java库路径下:将
libhellojni.so(Linux) 或hellojni.dll(Windows) 放在Java的lib目录下或系统PATH环境变量中包含的目录。 - 通过
-Djava.library.path指定:这是最推荐的方式。
# 在Linux/macOS上 java -Djava.library.path=. HelloJNI # 在Windows上 java -Djava.library.path=. HelloJNI
预期输出:
C: Received name from Java: from C
Java received from C: Hello, from C! I'm C.
关键JNI API总结
| 操作 | JNI函数 | 说明 |
|---|---|---|
| 获取类 | FindClass(env, "java/lang/String") |
通过类全名获取 jclass 对象。 |
| 获取方法ID | GetMethodID(env, clazz, "methodName", "(Ljava/lang/String;)V") |
获取实例方法ID。GetStaticMethodID 用于静态方法。 |
| 调用方法 | CallVoidMethod(env, obj, methodID, ...) |
调用返回值为void的方法,还有 CallIntMethod, CallObjectMethod 等。 |
| 创建对象 | NewObject(env, clazz, constructorID, ...) |
创建Java对象实例。 |
| 基本类型转换 | GetIntField, SetIntField |
直接操作Java对象的成员变量。 |
| 字符串操作 | NewStringUTF, GetStringUTFChars, ReleaseStringUTFChars |
创建和转换Java字符串。 |
| 异常处理 | ExceptionCheck(env), ExceptionDescribe(env) |
检查并打印JNI调用过程中发生的Java异常。 |
重要注意事项
- 性能开销:JNI调用比普通Java方法调用慢得多,频繁地在Java和C之间切换会严重影响性能,应尽量减少JNI调用的次数,在C中完成批量计算。
- 内存管理:C语言没有自动垃圾回收,当你通过JNI分配内存(如
GetStringUTFChars)或创建对象时,必须手动释放,否则会导致内存泄漏。 - 线程安全:
JNIEnv是线程绑定的,一个线程不能使用另一个线程的JNIEnv,如果你的C代码需要被多个Java线程调用,必须确保每个Java线程调用C函数时,C函数内部使用的是属于该线程自己的JNIEnv。 - 类型签名:JNI的类型签名(如
I代表int,Ljava/lang/String;代表String)是固定的,但容易写错,可以使用javap工具来查看:javap -s HelloJNI
- 异常处理:在C代码中调用JNI函数后,如果Java端抛出了异常,JNI函数会返回一个错误值或NULL,但异常状态会保留,在继续执行其他JNI操作前,必须先处理(通常是清除)这个异常,否则C代码会处于不稳定状态。
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); // 打印异常信息 (*env)->ExceptionClear(env); // 清除异常 // ... 错误处理逻辑 }
通过以上步骤,你就可以成功地在C语言中调用Java的方法了,这为跨语言混合开发提供了强大的能力。
