77、Java编程练习与JNI技术详解

Java编程练习与JNI技术详解

一、Java编程练习

1.1 练习题目概述

以下是一系列Java编程练习,涵盖了对象引用、克隆、异常处理、不可变类创建等多个方面,通过这些练习可以加深对Java编程的理解和掌握。

1.2 具体练习内容

  1. 二级别名演示 :创建一个方法,该方法接收一个对象引用,但不修改该引用指向的对象。不过,该方法会调用第二个方法,并将引用传递给它,而第二个方法会修改该对象。
  2. 自定义字符串类 :创建一个名为 myString 的类,该类包含一个 String 对象,在构造函数中使用构造函数的参数对其进行初始化。添加 toString() 方法和 concatenate() 方法, concatenate() 方法用于将一个 String 对象追加到内部字符串中。在 myString 类中实现 clone() 方法。创建两个静态方法,每个方法都接收一个 myString 类型的引用 x ,并调用 x.concatenate("test") ,但在第二个方法中先调用 clone() 方法。测试这两个方法并展示不同的效果。
  3. 电池与玩具类 :创建一个名为 Battery 的类,该类包含一个 int 类型的电池编号(作为唯一标识符)。使该类可克隆,并提供 toString() 方法。然后创建一个名为 Toy 的类,该类包含一个 Battery 数组和一个 toString() 方法,用于打印所有电池信息。为 Toy 类编写 clone() 方法,该方法会自动克隆其所有的 Battery 对象。通过克隆 Toy 对象并打印结果来测试该方法。
  4. 异常处理修改 :修改 CheckCloneable.java ,使所有的 clone() 方法捕获 CloneNotSupportedException ,而不是将其传递给调用者。
  5. 创建不可变类 :使用可变伴随类技术,创建一个包含 int double char 数组的不可变类。
  6. 性能测试修改 :修改 Compete.java ,为 Thing2 Thing4 类添加更多成员对象,观察性能随复杂度的变化情况,判断是简单的线性关系还是更复杂的关系。
  7. 深度复制蛇类 :从 Snake.java 开始,创建一个蛇类的深度复制版本。
  8. 继承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和本地代码之间的来回传递。
  • 及时释放不再使用的本地引用和全局引用,避免内存泄漏。
  • 对频繁调用的本地方法进行性能测试和优化。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值