时间:2025-08-08 07:41
人气:
作者:admin
Java与C++混合编程可以实现两种语言的优势结合,C++的程序性能很高且支持强大的系统调用能力,Java则生态丰富且开发效率较高。JNI是Java与C++进行混合编程的关键桥梁,本章将基于JNI技术讲述Java与C++混合编程的方法和技巧。
Java是一种高级编程语言,也是一个计算平台(通常指Java虚拟机)。最初由Sun Microsystems公司(后被Oracle收购)的James Gosling和他的团队在1995年发布。Java语言的设计目标是简单性、健壮性和跨平台兼容性。以下是Java的一些关键特点:
Write Once, Run Anywhere,WORA)。Java程序在执行前会被编译成字节码,这种中间形式的代码可以在任何安装了Java虚拟机(JVM)的设备上运行。Java的应用场景广泛,是目前最流行的后端系统开发语言,此外Java还是Android系统的主要编程语言,绝大部分的Android应用程序都基于Java语言进行开发。
JVM(Java Virtual Machine)是一个可以执行Java字节码的虚拟计算机。它是Java平台的核心组成部分,提供了Java程序运行所需的环境。JVM是Java语言能做到“一次编写,到处运行”的基础。以下是JVM的一些关键特点和功能:
JNI(Java Native Interface)是一个允许Java代码与本地代码(如:C/C++)进行交互的接口。通过JNI,Java应用程序可以调用本地库中的函数,也可以被本地代码调用,它是实现Java与C/C++混合编程的关键机制。
JNI主要包含以下两部分内容:
javah、javac等。JNI接口的官方文档:https://docs.oracle.com/en/java/javase/21/docs/specs/jni/index.html
本章所有的示例代码的开发环境如下:
在官网下载最新版本的安装包,官网下载地址:https://www.oracle.com/cn/java/technologies/downloads/

双击安装包,根据提示一步步安装即可。
打开命令行输入一下命令,验证是否安装成功,如果有显示相应的版本号则说明安装成功。
java -version
安装JDK:
# 1. 更新软件包列表
sudo apt update
# 3. 该命令将自动选择并安装最新的 LTS 版本,当前是 OpenJDK 21[5]。
sudo apt install default-jdk
# 3. 验证是否安装成功,如果有显示相应的版本号则说明安装成功。
java --version
设置环境变量:
# 1. 查找JDK的安装路径
update-alternatives --config java
There are 2 choices for the alternative java (providing /usr/bin/java).
Selection Path Priority Status
------------------------------------------------------------
* 0 /usr/lib/jvm/java-21-openjdk-amd64/bin/java 2111 auto mode
1 /usr/lib/jvm/java-11-openjdk-amd64/bin/java 1111 manual mode
# 2. vim打开.zshrc(如果你的SHELL用的是.bashrc,替换成相应的.bashrc)
vim ~/.zshrc
# 3. 在文件末尾添加如下内容
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH=${JAVA_HOME}/bin:${PATH}
# 4. 重新加载配置
source ~/.zshrc
以下是通过Homebrew工具的安装步骤,确保已经安装Homebrew。
# 1. 更新软件包列表
brew update
# 2.1 安装Oracle JDK的最新版本
brew install oracle-jdk
# 2.2 安装Open JDK的最新版本
brew install java
# 3. 验证是否安装成功,如果有显示相应的版本号则说明安装成功。
java --version
Java语言最初由Sun公司研发,并发布了Java SE(Standard Edition)的规范和开源的Open JDK。Sun公司后被Oracle公司收购,Oracle基于Open JDK开发了 Oracle JDK。
Open JDK是一个完全开源的项目,遵循GPL v2许可。任何人都可以下载、使用、修改和分发它的代码。主要的Linux发行版(如:Fedora,Ubuntu等)提供OpenJDK作为默认的Java SE实现。
Oracle JDK则基于Open JDK构建,但包含一些闭源组件,如Java插件、Java WebStart的实现和一些第三方组件。这些组件包括了一些商业功能,未开源。
Open JDK和Oracle JDK都遵循Java SE的规范,只是Oracle JDK提供了更多商业版的未开源的功能。
Say Hello程序SayHello.java新建一个say_hello的测试目录,然后在该目录下新建一个SayHello.java文件,并编写如下代码:
public class SayHello {
// 类方法
private native void sayHello(String name);
// 静态方法
private static native void sayGoodbye(String name);
static {
// 在程序初始化时加载native动态库(libhello.so)
System.loadLibrary("hello");
}
public static void main(String[] args) {
new SayHello().sayHello("Spencer");
SayHello.sayGoodbye("陌尘");
}
}
说明:
这里有两个被声明为native的方法,表示这两个方法需要native代码(C/C++)实现。这里一个是普通的类成员方法,一个是静态的类方法。
private native void sayHello(String name);
private static native void sayGoodbye(String name);
static包含的代码块,表示在程序初始化时加载native动态库(libhello.so)
static {
System.loadLibrary("hello");
}
SayHello.javajavac ./SayHello.java
执行完成后,会生成一个SayHello.class的字节码文件。
SayHello.h执行以下命令生成native代码的头文件
# JDK 9.0 之前
javah -cp ./ -d ./ SayHello
# `-cp ./`表示设置classpath为当前目录,在当前目录下查找.class文件
# `-d ./`表示设置头文件的输出目录为当前目录
# JDK 9.0 及之后
javac -h ./ ./SayHello.java
# 第一个`./` 表示设置头文件的输出目录为当前目录
执行成功后会在当前目录下生成SayHello.h头文件,内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class SayHello */
#ifndef _Included_SayHello
#define _Included_SayHello
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: SayHello
* Method: sayHello
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_SayHello_sayHello
(JNIEnv *, jobject, jstring);
/*
* Class: SayHello
* Method: sayGoodbye
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_SayHello_sayGoodbye
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
代码说明:
头文件中包含两个函数分别和.java中的两个方法一一对应。
函数声明中开通的部分JNIEXPORT void JNICALL,这个与《导出接口的定义》一文中的EAPI int CALLType是不是非常类似?是的,它就是JNI提供的动态库导出接口声明和调用约定声明。
函数的命名非常有规律,其实它是遵循了JNI的函数命名规范:
Java_{package_name}_{class_name}_{function_name}(JNI arguments)。
函数的参数
JNIEnv *env是一个指向JNI运行环境的指针,提供了JNI接口的各种功能函数。jobject obj,指代java中的this对象,可以通过该参数来获取Java对象的方法和属性。jclass cls,指代java中的类,可以通过该参数来获取Java类的静态方法和静态属性。.java中声明的方法的参数一一对应。SayHello.cp新建SayHello.cpp文件,并实现头文件声明的两个函数,内容如下:
#include "SayHello.h"
#include <iostream>
JNIEXPORT void JNICALL Java_SayHello_sayHello(JNIEnv* env, jobject obj, jstring name)
{
// 将jstring转化成C风格的UTF-9字符串
const char* cName = env->GetStringUTFChars(name, nullptr);
if (cName == nullptr)
{
return;
}
std::cout << "Hello, " << cName << "!" << std::endl;
}
JNIEXPORT void JNICALL Java_SayHello_sayGoodbye(JNIEnv* env, jclass cls, jstring name)
{
// 将jstring转化成C风格的UTF-9字符串
const char* cName = env->GetStringUTFChars(name, nullptr);
if (cName == nullptr)
{
return;
}
std::cout << "Goodbye, " << cName << "!" << std::endl;
}
SayHello.cppg++ -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux SayHello.cpp -o libhello.so
执行成功后会生成libhello.so文件。
SayHello程序java -Djava.library.path=./ SayHello
Hello, Spencer!
Goodbye, 陌尘!
-Djava.library.path=./表示在当前目录下查找libhello.so。
根据前面“Say Hello程序”的示例,可以总结出JNI开发的关键步骤和原理如下:
javah(或javac -h)工具从Java类生成C/C++头文件,该头文件包含JNI函数的原型。.so、.dll或.dylib文件),然后在Java程序中加载这些库。JNI定义了一套以j开头的C/C++的数据类型,与Java进行一一对应,他们之间的对应关系如下:
| 分类 | Java数据类型 | JNI数据类型 | C/C++数据类型 |
|---|---|---|---|
| 基础类型 | boolean | jboolean | unsigned char,相当于uint8_t。 |
| 基础类型 | byte | jbyte | signed char,相当于int8_t。 |
| 基础类型 | char | jchar | unsigned short,相当于uint16_t。 |
| 基础类型 | short | jshort | short,相当于int16_t。 |
| 基础类型 | int | jint | int,相当于int32_t。 |
| 基础类型 | long | jlong | long,相当于int64_t。 |
| 基础类型 | float | jfloat | float,4字节 |
| 基础类型 | double | jdouble | double,8字节 |
| 引用类型 | Object | jobject | jobject的定义:class _jobject {};typedef _jobject *jobject;所以jobject的作用类似于 void*,表示通用对象指针。 |
| 引用类型 | Class | jclass | class _jclass : public _jobject {};typedef _jclass *jclass; |
| 引用类型 | String | jstring | class _jstring : public _jobject {};typedef _jstring *jstring; |
| 引用类型 | 数组 | jarray | class _jarray : public _jobject {};typedef _jarray *jarray; |
| 引用类型 | Throwable | jthrowable | class _jthrowable : public _jobject {};typedef _jthrowable *jthrowable; |
Java的数据类型分基础数据类型(如int)和引用数据类型(如:Object、Class)。
基础数据类型: 会直接转换为C/C++的基础数据类型,例如int类型映射为jint类型。由于 jint是C/C++类型,所以可以直接当作普通C/C++变量使用,而不用做任何转换。
引用数据类型: 对象只会转换为一个C/C++指针,例如Object类型映射为jobject类型。由于指针指向Java虚拟机内部的数据结构,所以不可能直接在C/C++代码中操作对象,而是需要依赖JNIEnv环境对象。另外,为了避免对象在使用时突然被回收,在本地方法返回前,虚拟机会固定(pin)对象,阻止其 GC。
Java中的数组对应于C/C++中的jarray,它是一个通用的数组类型。而具体数据类型的数组,对应于Java数组的特定类型,对应关系如下。
| Java数据类型 | JNI数据类型 |
|---|---|
| boolean[] | jbooleanArray |
| byte[] | jbyteArray |
| char[] | jcharArray |
| short[] | jshortArray |
| int[] | jintArray |
| long[] | jlongArray |
| float[] | jfloatArray |
| double[] | jdoubleArray |
| Object[] | jobjectArray |
历史文章推荐:
21. 工程篇:VSCode中使用CMake插件运行和调试程序
28. 跨语言:C/C++与JavaScript的WebAssembly编程(一)
29. 跨语言:C/C++与JavaScript的WebAssembly编程(二)
30. 跨语言:C/C++与JavaScript的WebAssembly编程(三)
36. 跨语言:C/C++与Swift&Objective-C混合编程(一)
大家好,我是陌尘。
IT从业10年+, 北漂过也深漂过,目前暂定居于杭州,未来不知还会飘向何方。
搞了8年C++,也干过2年前端;用Python写过书,也玩过一点PHP,未来还会折腾更多东西,不死不休。
感谢大家的关注,期待与你一起成长。
扫码二维码,关注微信公众号,阅读更多精彩内容