【Java高级】在Java中运行C++代码的七大步骤(native 方法)

本文章参考资料

目录

基本概述

本地方法实现步骤

Step1.VS 创建C++项目:动态链接库 DLL

Step2.编写 Java 方法

Step3.经过一系列编译,得到 .h 头文件

Step4.引入所需的头文件到C++头文件项目下

Step5.编写 C++ 代码

Step6.右键C++项目生成 .dll

Step7.将生成的 .dll 文件,放在 Step2 中System.load所指定的目录下


基本概述

JNI介绍

JNI是Java Native Interface的缩写,中文为JAVA本地调用。从Java 1.1 开始,Java Native Interface (JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。

使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的,比如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少保证本地代码能工作在任何Java 虚拟机实现下。

JNI的使用场景

Java应用需要与本地其它非Java应用交互 遇到性能瓶颈,通常本地代码执行效率要优于Java代码 JDK提供的功能不足以帮我们实现需要的功能

JNI的问题

因为JNI有一个Native这个特点,一点有项目用了JNI,也就说明这个项目基本不能跨平台了。 JNI调用是相当慢的,在实际使用的之前一定要先想明白是否有这个必要。 因为C++和C这样的语言非常灵活,一不小心就容易出错,比如代码就没有写析构字符串释放内存,就存在内存泄漏的问题,对于java developer来说因为有了GC 垃圾回收机制,所以大多数人没有写析构函数这样的概念。所以JNI也会增加程序中的风险,增大程序的不稳定性


本地方法实现步骤

我们以 Java 调用本地 C++ 方法为例,阐述本方法的应用

主要分为以下几个步骤

1. 编写带有native声明的方法的java类
2. 使用javac命令编译所编写的java类
3. 使用javah -jni 类的全限定名 生成扩展名为h的头文件
4. 使用C/C++实现本地方法
5. 将C/C++编译的文件生成动态连接库(dll文件)
6. 在java项目中引入生成的C/C++库文件

Step1.VS 创建C++项目:动态链接库 DLL

Step2.编写 Java 方法

声明native方法:如果你想将一个方法做为一个本地方法的话,那么你就必须声明改方法为 native的,并且不能实现。

Load动态库:System.loadLibrary(“hello”);加载动态库(我们可以这样理解:我们的方法 displayHelloWorld()没有实现,但是我们在下面就直接使用了,所以必须在使用之前对它进行初始化)这里一般是以static块进行加载的。同时需要注意的是System.loadLibrary();的参数“hello”是动态库的名字。

public class NativeMethod {
    public native int nativeMethod(String str);


    static{
        //参数名字是:C++ 中的动态库的路径+名字,即 .dll文件路径
        System.loadLibrary(待定,这里写的是待会儿生成的 .dll文件的路径);
    }


    public static void main(String[] args) {
        System.out.println("JAVA输出的消息:准备执行 nativeMethod");
        String str = "这个字符串来自Java";
        System.out.println(new NativeMethod().nativeMethod(str));
    }
}

Step3.经过一系列编译,得到 .h 头文件

在.java 所在目录进入CMD javac [类名.java] 得到当前类的 .class 文件

在.class 所在目录进入CMD,输入 javac -encoding utf8 -h . [类名.java] 得到当前类的 .h 文件

生成的 .h 文件内容如下

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class NativeMethod */

#ifndef _Included_NativeMethod
#define _Included_NativeMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     NativeMethod
 * Method:    nativeMethod
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_NativeMethod_nativeMethod
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

我们主要需要关注的是这段代码,并 在C++中定义该函数

JNIEXPORT jint JNICALL Java_NativeMethod_nativeMethod
  (JNIEnv *, jobject, jstring);

Step4.引入所需的头文件到C++头文件项目下

将Step3中得到的Java 的 .h文件复制到 C++ 代码目录下

 

%JAVA_HOME%/include目录下的“jni.h”%JAVA_HOME%/include/win32目录下的“jni_md.h”两 个文件复制到Hello目录下,然后和上述步骤一样添加到当前 C++ 项目的头文件中

将 Java生成的 .h 的#include <jni.h> 修改为#include "jni.h"

Step5.编写 C++ 代码

右键 源文件-->添加-->新建项,选择.cpp文件,随便命名

引入头文件 NativeMethod.h(就是Java生成的那个头文件)

#include <iostream>
#include "pch.h"
#include "NativeMethod.h"
#include <iostream>


JNIEXPORT jint JNICALL Java_NativeMethod_nativeMethod
(JNIEnv*, jobject, jstring s) {
	std::cout << "C++输出的消息:接收到了来自Java程序的" << s << std::endl;
	return 10;
}

Step6.右键C++项目生成 .dll

Tips:生成之前需要注意一下编译的位数。如果电脑是 64位的就在 x64 的状态下编译。否则如果 64位电脑生成了 32位的 .dll,是无法使用的

 

Step7.将生成的 .dll 文件,放在 Step2 中System.load所指定的目录下

首先,把 .dll 所在的路径填入 Step2 中的 System.load 中

public class NativeMethod {
    public native int nativeMethod(String str);


    static{
        /**
         * 写一段Java代码。由于我们在Windows环境下,所以用的是一 个.dll文件,
         * 如果在Linux环境下的话,用的是一个.so文件。
         * 最后C++代码写完之后要生成一个.dll/.so文件,生成的文件可以使用 static静态加载的方法加载进来,也可以通过配置环境变量的方式,这里选择前者
         */
        //参数名字是:系统文件 .dll 的路径名(.dll 路径是在 C++中生成的一个系统文件,作用是让 Java能够读取操作系统的接口,从而运行我们写的C++代码)
        System.load("C:\\Local_DeskData\\Program\\Codes\\JavaCodes\\BaseCodes\\Step11(Others)\\src\\" + "Dll1.dll");
    }


    public static void main(String[] args) {
        System.out.println("JAVA输出的消息:准备执行 nativeMethod");
        String str = "这个字符串来自Java";
        System.out.println("C++程序返回的结果 = " + new NativeMethod().nativeMethod(str));
    }
}

 运行

可以看到,虽然有乱码,但起码程序还是运行成功了的。

这说明我们本地方法成功运行了 C++ 代码

你问我乱码怎么解决?鬼知道呢…… 可能是使用二进制传输?反正网上搜不到……

有什么心得?

我们可以从这7个步骤中感悟到什么?

1、1个类中有很多Native方法-->这个类中的所有Native方法生成到1个.h文件中-->本地代码生成一个.dll/.so文件和一个类的Native方法实现相对应

2、为什么有Native方法的类中必有这么一段代码

private static native void ();
static {
    registerNatives();
}

  现在想来,估计和我们的静态代码块起的作用一样,都是为这个类导入特定的.dll/.so文件用的。至于为什么不能像我们这么写,个人猜测,是因为不同的用户磁盘上的.dll/.so文件位置不固定,和JDK安装目录相关?

  3、Java不在乎Native方法是用什么语言实现的,只要一来语言能和底层打 交道就好了,二来语言实现完可以提供出来.dll/.so文件。因此同一个Native方法,如果不同的Java虚拟机去调用它,那么结果可能都不同,比 如Object的hashCode(),当然,运行效率也不尽然相同,因为不同的虚拟机对于不同的Native方法有自己的实现。

使用native关键字目的

 为什么要使用Native Method?

  java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。   与java环境外交互:    有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。   与操作系统交互:    JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎 样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。   Sun's Java    Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。

总的来说,JAVA的native方法适用的情况:   

1、为了使用底层的主机平台的某个特性,而这个特性不能通过JAVA API访问。   

2、为了访问一个老的系统或者使用一个已有的库,而这个系统或这个库不是用JAVA编写的。

3、为了加快程序的性能,而将一段时间敏感的代码作为本地方法实现。

native关键字特点

  标识符native可以与所有其它的java标识符连用,但是abstract除外。这是合理的,因为native暗示这些方法是有实现体的,只不过这些实现体是非java的,但是abstract却显然的指明这些方法无实现体。native与其它java标识符连用时,其意义同非Native Method并无差别,比如native static表明这个方法可以在不产生类的实例时直接调用,这非常方便,比如当你想用一个native method去调用一个C的类库时。上面的第三个方法用到了native synchronized,JVM在进入这个方法的实现体之前会执行同步锁机制(就像java的多线程。)

  一个native  method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制。这些方法的实现体可以制一个异常并且将其抛出,这一点与java的方法非常相似。当一个native  method接收到一些非基本类型时如Object或一个整型数组时,这个方法可以访问这非些基本型的内部,但是这将使这个native方法依赖于你所访问的java类的实现。有一点要牢牢记住:我们可以在一个native  method的本地实现中访问所有的java特性,但是这要依赖于你所访问的java特性的实现,而且这样做远远不如在java语言中使用那些特性方便和容易。
  native  method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。需要注意当我们将一个本地方法声明为final的情况。用java实现的方法体在被编译时可能会因为内联而产生效率上的提升。但是一个native final方法是否也能获得这样的好处却是值得怀疑的,但是这只是一个代码优化方面的问题,对功能实现没有影响。
  如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法(这个似乎看起来有些奇怪),同样的如果一个本地方法被fianl标识,它被继承后不能被重写。

  本地方法非常有用,因为它有效地扩充了jvm.事实上,我们所写的java代码已经用到了本地方法,在sun的java的并发(多线程)的机制实现中,许多与操作系统的接触点都用到了本地方法,这使得java程序能够超越java运行时的界限。有了本地方法,java程序可以做任何应用层次的任务。

  • 11
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Graskli

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值