Java编程练习与JNI技术详解
一、Java编程练习
1.1 练习题目概述
以下是一系列Java编程练习,涵盖了对象引用、克隆、异常处理、不可变类创建等多个方面,通过这些练习可以加深对Java编程的理解和掌握。
1.2 具体练习内容
- 二级别名演示 :创建一个方法,该方法接收一个对象引用,但不修改该引用指向的对象。不过,该方法会调用第二个方法,并将引用传递给它,而第二个方法会修改该对象。
-
自定义字符串类
:创建一个名为
myString的类,该类包含一个String对象,在构造函数中使用构造函数的参数对其进行初始化。添加toString()方法和concatenate()方法,concatenate()方法用于将一个String对象追加到内部字符串中。在myString类中实现clone()方法。创建两个静态方法,每个方法都接收一个myString类型的引用x,并调用x.concatenate("test"),但在第二个方法中先调用clone()方法。测试这两个方法并展示不同的效果。 -
电池与玩具类
:创建一个名为
Battery的类,该类包含一个int类型的电池编号(作为唯一标识符)。使该类可克隆,并提供toString()方法。然后创建一个名为Toy的类,该类包含一个Battery数组和一个toString()方法,用于打印所有电池信息。为Toy类编写clone()方法,该方法会自动克隆其所有的Battery对象。通过克隆Toy对象并打印结果来测试该方法。 -
异常处理修改
:修改
CheckCloneable.java,使所有的clone()方法捕获CloneNotSupportedException,而不是将其传递给调用者。 -
创建不可变类
:使用可变伴随类技术,创建一个包含
int、double和char数组的不可变类。 -
性能测试修改
:修改
Compete.java,为Thing2和Thing4类添加更多成员对象,观察性能随复杂度的变化情况,判断是简单的线性关系还是更复杂的关系。 -
深度复制蛇类
:从
Snake.java开始,创建一个蛇类的深度复制版本。 -
继承ArrayList实现深度复制
:继承
ArrayList,并使其clone()方法执行深度复制。
1.3 练习总结
这些练习涉及到Java编程的多个重要概念,包括对象引用、克隆、异常处理、不可变类等。通过完成这些练习,可以提高对Java语言的掌握程度,加深对这些概念的理解。
二、Java Native Interface (JNI) 介绍
2.1 JNI概述
Java语言及其标准API功能丰富,足以编写完整的应用程序。但在某些情况下,需要调用非Java代码,例如访问特定操作系统的功能、与特殊硬件设备交互、重用现有的非Java代码库或实现对时间要求严格的代码段。与非Java代码进行交互需要编译器和虚拟机的专门支持,以及将Java代码映射到非Java代码的额外工具。JavaSoft提供的调用非Java代码的标准解决方案是Java Native Interface (JNI)。
2.2 JNI功能
JNI是一个相当丰富的编程接口,允许从Java应用程序中调用本地方法。它在Java 1.1中被引入,并与Java 1.0的等效接口(本地方法接口NMI)保持了一定程度的兼容性。但由于NMI的设计特点使其不适合在所有虚拟机中采用,因此未来的Java版本可能不再支持NMI。目前,JNI设计用于与仅用C或C++编写的本地方法进行交互。使用JNI,本地方法可以:
- 创建、检查和更新Java对象(包括数组和字符串)
- 调用Java方法
- 捕获和抛出异常
- 加载类并获取类信息
- 执行运行时类型检查
2.3 调用本地方法示例
下面通过一个简单的示例来演示如何从Java程序中调用本地方法,该本地方法又会调用标准C库函数
printf()
。
2.3.1 编写Java代码
首先,编写Java代码声明一个本地方法及其参数:
//: appendixb:ShowMessage.java
public class ShowMessage {
private native void ShowMessage(String msg);
static {
System.loadLibrary("MsgImpl");
// Linux hack, if you can't get your library
// path set in your environment:
// System.load(
// "/home/bruce/tij2/appendixb/MsgImpl.so");
}
public static void main(String[] args) {
ShowMessage app = new ShowMessage();
app.ShowMessage("Generated with JNI");
}
} ///:~
在上述代码中,本地方法声明后面跟着一个静态块,调用
System.loadLibrary()
方法加载一个动态链接库(DLL)到内存中并进行链接。DLL必须位于系统库路径中,文件名扩展名会根据平台由JVM自动添加。另外,还可以使用
System.load()
方法,该方法接受一个绝对路径,但使用环境变量是更好、更具可移植性的解决方案。
2.3.2 生成头文件
编译Java源文件后,使用
javah
工具对生成的
.class
文件运行,并指定
—jni
开关:
javah —jni ShowMessage
javah
会读取Java类文件,并为每个本地方法声明在C或C++头文件中生成一个函数原型。以下是生成的
ShowMessage.h
文件:
/* DO NOT EDIT THIS FILE
- it is machine generated */
#include <jni.h>
/* Header for class ShowMessage */
#ifndef _Included_ShowMessage
#define _Included_ShowMessage
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: ShowMessage
* Method: ShowMessage
* Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL
Java_ShowMessage_ShowMessage
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
通过
#ifdef __cplusplus
预处理器指令可以看出,该文件可以由C或C++编译器编译。第一个
#include
指令包含
jni.h
头文件,该头文件定义了文件中使用的类型。
JNIEXPORT
和
JNICALL
是宏,会扩展为匹配特定平台的指令。
JNIEnv
、
jobject
和
jstring
是JNI数据类型定义。
2.3.3 实现DLL
接下来,编写C或C++源文件,包含
javah
生成的头文件并实现本地方法,然后编译生成动态链接库。以下是
MsgImpl.cpp
文件:
//: appendixb:MsgImpl.cpp
//# Tested with VC++ & BC++. Include path must
//# be adjusted to find the JNI headers. See
//# the makefile for this chapter (in the
//# downloadable source code) for an example.
#include <jni.h>
#include <stdio.h>
#include "ShowMessage.h"
extern "C" JNIEXPORT void JNICALL
Java_ShowMessage_ShowMessage(JNIEnv* env,
jobject, jstring jMsg) {
const char* msg=env->GetStringUTFChars(jMsg,0);
printf("Thinking in Java, JNI: %s\n", msg);
env->ReleaseStringUTFChars(jMsg, msg);
} ///:~
传递给本地方法的参数是返回Java的网关。第一个参数类型为
JNIEnv
,包含所有允许回调到JVM的钩子。第二个参数根据方法类型有不同的含义,对于非静态方法,它相当于C++中的
this
指针,类似于Java中的
this
,是调用本地方法的对象的引用;对于静态方法,它是实现该方法的类对象的引用。其余参数表示传递给本地方法调用的Java对象,基本类型也是通过这种方式传递,但它们是按值传递的。
2.4 JNI函数访问
2.4.1 JNIEnv参数
JNI函数是程序员在本地方法中与JVM进行交互的工具。每个JNI本地方法都会接收一个特殊的参数
JNIEnv
作为第一个参数,它是指向
JNIEnv_
类型特殊JNI数据结构的指针。该数据结构的一个元素是指向JVM生成的数组的指针,数组的每个元素是指向JNI函数的指针。可以通过解引用这些指针从本地方法中调用JNI函数。
2.4.2 JNI函数分类
通过
JNIEnv
参数,程序员可以访问大量的函数,这些函数可以分为以下几类:
- 获取版本信息
- 执行类和对象操作
- 处理Java对象的全局和本地引用
- 访问实例字段和静态字段
- 调用实例方法和静态方法
- 执行字符串和数组操作
- 生成和处理Java异常
2.5 访问Java字符串
Java字符串采用Unicode格式,如果要将其传递给非Unicode函数(如
printf()
),必须先使用
GetStringUTFChars()
函数将其转换为ASCII字符。该函数将Java字符串转换为UTF - 8字符。以下是
MsgImpl.cpp
中访问Java字符串的示例:
const char* msg=env->GetStringUTFChars(jMsg,0);
printf("Thinking in Java, JNI: %s\n", msg);
env->ReleaseStringUTFChars(jMsg, msg);
2.6 传递和使用Java对象
可以将自己创建的Java对象传递给本地方法,并在本地方法中访问该对象的字段和方法。以下是一个示例:
2.6.1 Java代码
//: appendixb:UseObjects.java
class MyJavaClass {
public int aValue;
public void divByTwo() { aValue /= 2; }
}
public class UseObjects {
private native void
changeObject(MyJavaClass obj);
static {
System.loadLibrary("UseObjImpl");
// Linux hack, if you can't get your library
// path set in your environment:
// System.load(
//"/home/bruce/tij2/appendixb/UseObjImpl.so");
}
public static void main(String[] args) {
UseObjects app = new UseObjects();
MyJavaClass anObj = new MyJavaClass();
anObj.aValue = 2;
app.changeObject(anObj);
System.out.println("Java: " + anObj.aValue);
}
} ///:~
2.6.2 C++代码
//: appendixb:UseObjImpl.cpp
//# Tested with VC++ & BC++. Include path must
//# be adjusted to find the JNI headers. See
//# the makefile for this chapter (in the
//# downloadable source code) for an example.
#include <jni.h>
extern "C" JNIEXPORT void JNICALL
Java_UseObjects_changeObject(
JNIEnv* env, jobject, jobject obj) {
jclass cls = env->GetObjectClass(obj);
jfieldID fid = env->GetFieldID(
cls, "aValue", "I");
jmethodID mid = env->GetMethodID(
cls, "divByTwo", "()V");
int value = env->GetIntField(obj, fid);
printf("Native: %d\n", value);
env->SetIntField(obj, fid, 6);
env->CallVoidMethod(obj, mid);
value = env->GetIntField(obj, fid);
printf("Native: %d\n", value);
} ///:~
要访问Java字段或方法,必须先使用
GetFieldID()
和
GetMethodID()
函数获取其标识符。这些函数接受类对象、元素名称字符串和类型信息字符串,返回一个用于访问元素的标识符。
2.7 JNI与Java异常
使用JNI时,可以像在Java程序中一样抛出、捕获、打印和重新抛出Java异常,但需要程序员调用专门的JNI函数来处理异常。以下是一些用于异常处理的JNI函数:
-
Throw()
:抛出一个现有的异常对象,用于在本地方法中重新抛出异常。
-
ThrowNew()
:生成一个新的异常对象并抛出。
-
ExceptionOccurred()
:确定是否抛出了异常且尚未清除。
-
ExceptionDescribe()
:打印异常和堆栈跟踪信息。
-
ExceptionClear()
:清除待处理的异常。
-
FatalError()
:引发致命错误,不返回。
在调用每个JNI函数后,必须调用
ExceptionOccurred()
来检查是否抛出了异常。如果检测到异常,必须确保最终清除该异常,否则调用JNI函数的结果将不可预测。
2.8 JNI与线程
由于Java是多线程语言,多个线程可以同时调用本地方法。程序员必须确保本地调用是线程安全的,即不会以无监控的方式修改共享数据。可以通过将本地方法声明为
synchronized
或在本地方法中实现其他策略来确保正确的并发数据操作。另外,不要跨线程传递
JNIEnv
指针,因为它指向的内部结构是按线程分配的,包含仅在特定线程中有意义的信息。
2.9 使用现有代码库
如果有一个现有的大型代码库需要从Java调用,将DLL中的所有函数重命名以匹配JNI名称修饰约定不是一个可行的解决方案。最佳方法是在原始代码库“外部”编写一个包装DLL,Java代码调用这个新DLL中的函数,而新DLL又调用原始DLL函数。
2.10 JNI使用流程总结
以下是使用JNI的主要流程:
1. 在Java类中声明本地方法。
2. 使用
System.loadLibrary()
或
System.load()
加载动态链接库。
3. 编译Java源文件,使用
javah
工具生成头文件。
4. 编写C或C++源文件,包含头文件并实现本地方法。
5. 编译C或C++源文件,生成动态链接库。
6. 在本地方法中使用
JNIEnv
参数访问JNI函数,处理Java对象、字符串、异常等。
2.11 总结
Java Native Interface (JNI) 提供了一种在Java应用程序中调用非Java代码的方式,通过JNI可以访问操作系统特定功能、与特殊硬件设备交互等。但使用JNI需要注意异常处理、线程安全等问题。同时,对于现有代码库的调用,可以采用包装DLL的方式。通过本文的介绍和示例代码,希望读者能够对JNI有更深入的理解和掌握。
2.12 参考代码流程图
graph TD;
A[编写Java代码声明本地方法] --> B[加载动态链接库];
B --> C[编译Java源文件];
C --> D[使用javah生成头文件];
D --> E[编写C或C++源文件实现本地方法];
E --> F[编译C或C++源文件生成动态链接库];
F --> G[在本地方法中使用JNIEnv访问JNI函数];
2.13 JNI函数分类表格
| 分类 | 说明 |
|---|---|
| 获取版本信息 | 获取JNI和Java虚拟机的版本信息 |
| 执行类和对象操作 | 创建、检查和更新Java对象 |
| 处理Java对象的全局和本地引用 | 管理Java对象的引用,确保对象在本地方法调用期间不被垃圾回收 |
| 访问实例字段和静态字段 | 获取和设置Java对象的实例字段和静态字段 |
| 调用实例方法和静态方法 | 调用Java对象的实例方法和静态方法 |
| 执行字符串和数组操作 | 处理Java字符串和数组 |
| 生成和处理Java异常 | 抛出、捕获和处理Java异常 |
2.14 本地方法参数传递机制
在JNI中,本地方法的参数传递机制有其独特之处。下面详细分析不同类型参数的传递方式:
-
基本类型参数
:基本类型(如
int
、
double
等)按值传递给本地方法。这意味着在本地方法中对这些参数的修改不会影响到Java端的原始值。例如,如果在Java代码中传递一个
int
类型的变量给本地方法,本地方法接收到的是该变量的副本。
-
对象类型参数
:对象类型参数传递的是Java对象的引用。在本地方法中,可以通过这个引用访问和修改Java对象的状态。但需要注意的是,本地方法中对对象的修改会直接反映到Java端的原始对象上。例如,传递一个
StringBuilder
对象给本地方法,在本地方法中调用其
append()
方法,Java端的
StringBuilder
对象也会被修改。
2.15 本地方法返回值处理
本地方法可以返回不同类型的值给Java代码。返回值的处理方式取决于返回值的类型:
-
基本类型返回值
:本地方法返回基本类型时,会直接将值返回给Java代码。例如,本地方法返回一个
int
类型的值,Java代码可以直接接收并使用。
-
对象类型返回值
:本地方法返回对象类型时,返回的是Java对象的引用。Java代码可以通过这个引用操作返回的对象。例如,本地方法返回一个
ArrayList
对象,Java代码可以对该
ArrayList
进行遍历、添加元素等操作。
2.16 JNI性能考虑
使用JNI虽然可以实现Java与非Java代码的交互,但也会带来一定的性能开销。以下是一些影响JNI性能的因素及相应的优化建议:
-
方法调用开销
:从Java代码调用本地方法会有一定的开销,因为涉及到Java和本地代码之间的上下文切换。为了减少这种开销,可以尽量减少不必要的本地方法调用,将相关操作合并到一个本地方法中。
-
数据传递开销
:在Java和本地代码之间传递数据也会有开销,尤其是传递大量数据时。可以考虑在本地代码中处理数据,减少数据在Java和本地代码之间的来回传递。
-
内存管理开销
:JNI中的内存管理需要特别注意,不当的内存管理会导致内存泄漏和性能问题。要及时释放不再使用的本地引用和全局引用,避免内存泄漏。
2.17 JNI实际应用场景
JNI在实际开发中有很多应用场景,以下是一些常见的例子:
-
访问操作系统特定功能
:当Java标准API无法满足需求时,可以使用JNI调用操作系统的底层功能。例如,在Windows系统中调用Windows API来实现文件操作、系统信息获取等功能。
-
与硬件设备交互
:在嵌入式系统开发中,需要与硬件设备进行交互。可以使用JNI编写本地代码来控制硬件设备,如读取传感器数据、控制电机等。
-
重用现有代码库
:如果有现有的非Java代码库(如C或C++代码库),可以使用JNI将其集成到Java应用程序中,避免重复开发。
2.18 JNI常见错误及解决方法
在使用JNI的过程中,可能会遇到一些常见的错误。以下是一些常见错误及相应的解决方法:
| 错误类型 | 错误描述 | 解决方法 |
| ---- | ---- | ---- |
|
UnsatisfiedLinkError
| 当Java代码无法加载本地库时会抛出该异常。 | 检查本地库的路径是否正确,确保本地库文件存在于系统库路径中。 |
|
NullPointerException
| 在本地方法中访问空引用时会抛出该异常。 | 在使用引用之前,先检查引用是否为空。 |
|
ClassNotFoundException
| 当本地方法无法找到Java类时会抛出该异常。 | 检查Java类的名称是否正确,确保Java类已经被正确加载。 |
|
NoSuchFieldError
或
NoSuchMethodError
| 当本地方法无法找到Java类的字段或方法时会抛出该异常。 | 检查字段或方法的名称和签名是否正确。 |
2.19 JNI开发环境搭建
搭建JNI开发环境需要以下步骤:
1.
安装Java开发工具包(JDK)
:确保已经安装了合适版本的JDK,并配置好环境变量。
2.
安装C或C++开发工具
:根据需要选择合适的C或C++开发工具,如Visual Studio、GCC等。
3.
配置开发环境
:将JDK的头文件路径和库文件路径添加到C或C++开发工具的包含路径和库路径中。
4.
编写Java代码和本地代码
:按照前面介绍的步骤编写Java代码和本地代码。
5.
编译和运行
:编译Java代码和本地代码,生成动态链接库,并运行Java程序。
2.20 JNI未来发展趋势
随着Java技术的不断发展,JNI也在不断改进和完善。未来,JNI可能会朝着以下方向发展:
-
性能优化
:进一步优化JNI的性能,减少方法调用和数据传递的开销。
-
简化开发
:提供更简单、更易用的API,降低JNI开发的难度。
-
跨平台支持
:增强JNI的跨平台支持能力,使其在不同操作系统和硬件平台上都能更好地工作。
2.21 总结
本文详细介绍了Java Native Interface (JNI) 的相关知识,包括JNI的概述、功能、调用本地方法的示例、JNI函数访问、异常处理、线程安全等方面。同时,还讨论了JNI的性能考虑、实际应用场景、常见错误及解决方法、开发环境搭建等内容。通过学习本文,读者可以对JNI有更深入的理解,并能够在实际开发中灵活运用JNI技术实现Java与非Java代码的交互。
2.22 JNI开发流程mermaid图
graph LR
A[安装JDK和C/C++开发工具] --> B[配置开发环境]
B --> C[编写Java代码声明本地方法]
C --> D[加载动态链接库]
D --> E[编译Java源文件]
E --> F[使用javah生成头文件]
F --> G[编写C或C++源文件实现本地方法]
G --> H[编译C或C++源文件生成动态链接库]
H --> I[运行Java程序调用本地方法]
2.23 JNI性能优化策略列表
- 减少不必要的本地方法调用,将相关操作合并到一个本地方法中。
- 在本地代码中处理数据,减少数据在Java和本地代码之间的来回传递。
- 及时释放不再使用的本地引用和全局引用,避免内存泄漏。
- 对频繁调用的本地方法进行性能测试和优化。
超级会员免费看
9660

被折叠的 条评论
为什么被折叠?



