Sun-JNI文档系列之四——第三章:基本类型、字符串类型与数组类型

来源:互联网 发布:mysql删除语句 编辑:程序博客网 时间:2024/06/06 00:39
          Java应用链接本地代码时(when interfacing Java applications with native code ),程序员们常常会有共同的一个疑问,那就是Java中的数据类型怎么与C/C++中的数据类型相互映射(map)?在上一章最后展示的“Hello World!”程序中,我们没有向本地代码传递任何参数,本地方法也没有返回任何返回值。本地方法只是简单地打印一条信息然后就 return了。
          实际上,绝大多数程序需要向native方法传递参数,同时也接收nativ方法的返回值。在这一章中,我们将介绍如何在 Java代码和实现native方法的native代码之间进行数据类型的转换。我们将从基本类型如integer以及普通对象类型如stringarray开始。我们把对任意对象的处理方式放到下一章讲解,在下一章我们将讲解native代码怎样访问对象那个字段和调用方法(access fields and make method calls)。

3.1 一个简单的native方法

          让我们从一个与上一章的HelloWorld区别不大的简单实例开始。示例程序Prompt.java包含一个打印字符串的native方法,等待用户输入然后返回用户输入的一行字符串(line)。这个程序的源代码如下所示:
                   class Prompt {
                            // native method that prints a prompt and reads a line
                            private native String getLine(String prompt);
                            public static void main(String args[]) {
                                     Prompt p = new Prompt();
                                     String input = p.getLine("Type a line: ");
                                     System.out.println("User typed: " + input);
                            }
                            static {
                                     System.loadLibrary("Prompt");
                            }
                   }
         Prompt.main调用native方法Prompt.getLine来接受用户输入。静态初始化程序调用System.loadLibrary方法加载叫做Promptnative库。

3.1.1 要实现的native方法的 C原型

         Prompt.getLine方法在下面的 C函数中被实现:
                   JNIEXPORT jstring JNICALL
                   Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt)
          你可以使用javah工具来生成包含上面函数原型的头文件。JNIEXPORTJNICALL宏(在jni.h头文件中定义)确保这个函数是从native库中读取的(is exported from the native libirary)以及 C编译器遵循正确的调用约定为这个函数生成代码。C函数的名称由“Java_”前缀、类名和方法名组成。在 11.3这一节中包含了对C函数命名的更详细的讲述。

3.1.2 native方法参数(arguement

          正如在2.4节中简单提到过的,native方法实现如Java_Prompt_getLine除了native方法中声明的参数外,还传入了两个标准参数(standard parameters)。第一个参数,JNIEnv接口指针,指向一个指针与函数的映射表(point to a location that contains a pointer to a function table)。函数表中的每一个入口指向一个 JNI函数。native方法通过其中的JNI函数访问Java虚拟机中的数据结构。图 3.1说明了JNIEnv接口指针。
          
3.1 JNI接口指针
          第二个参数是有所不同的,由native方法时静态方法还是实例方法决定。实例(instancenative 方法中的第二个参数是一个指向所调用方法所在对象的引用(a reference to the object on which the method is invoked),与C++中的this指针类似。静态native方法中的第二个参数是一个指向定义此方法的类的引用。在我们的Java_Prompt_getLine实例中,实现了一个实例native方法。

3.1.3 类型映射

         native方法声明中的参数类型与native语言中的类型一致。JNI中定义了一系列CC++类型与Java中的类型相统一。
          Java中有两种类型:基本类型(intfloat char)和引用类型(classesinstances arrays)。在 Java中,字符串是java.lang.String.class的实例。
         JNI对基本类型和引用类型的处理方式不同。基本类型的映射是直接的。例如,java中的int类型映射到C/C++类型jint(在jni.h中定义为 32位有符号整数),而java中的 float对应C/C++类型jfloat(在jni.h中定义为32位浮点数)。12.1.1这一节中包含了所有基本类型在 JNI中的定义。
         JNI将对象作为(opaque reference)传给native方法。opaque referenceC指针类型,指向Java虚拟机内部的数据结构。然而,内部数据结构的准确布局(exact layout)是对程序员隐藏的。native代码必须通过合适的 JNI函数执行对底层对象(the underlying object)的操作,这些函数可以通过 JNIEnv接口指针使用。例如java.lang.String对应的 JNI类型是jstring jstring引用的准确的值对 native代码无关的(the exact value of a jstring)。 native代码调用JNI函数,比如 GetStringUTFChars3.2.1),来访问字符串内容。
          所有JNI引用都有jobject类型(All JNI reference have type jobject)。为了方便和增强类型安全,JNI定义了一组引用类型,可以从概念上看作是jobject的子类型(conceptually subtypes of jobject)。(如果A B的每个实例的子类型,那么A也是B的实例)这些子类型对应Java中常用的引用类型。例如,jstring表示字符串;jobjectArray表示一个对象数组(an array of object)。12.2节中包含了完整的 JNI引用类型与他们的子类型的关系列表。

3.2 访问字符串

         Java_Prompt_getLine函数接收prompt参数作为jstring类型。jstring类型在Java虚拟机中表示字符串,与常规的C语言字符串类型不同(指向连续字符的指针,char *)。你不能将jstring作为正常的C语言字符串使用。如果运行下面的代码将不会产生预期的结果。实际上,更有可能使Java虚拟机死机重启。
                   JNIEXPORT jstring JNICALL
                   Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
                   {
                            /* ERROR: incorrect use of jstring as a char* pointer */
                            printf("%s", prompt);
                            ...
                   }

3.2.1 转换为native字符串

          你的native代码必须使用合适的JNI函数将jstring对象转换为C/C++字符串。JNI支持UnicodeUTF-8字符串的相互转换。Unicode字符串使用用16位的值表示单个字符,然而,UTF-8字符串采用与7ASCII码向上兼容的编码体制。(whereas UTF-8 strings use an encoding scheme that is upward compatible with 7-bit ASCII strings.UTF-8字符串像以NULL做终结符的C字符串,即使UTF-8字符串包含非ASCII字符。在UTF-8编码中所有编码值在1127之间的ASCII字符保持不变。高位的1个字节(A byte with the highest bit set signals the beginning of a multi-byte encoded 16-bit Unicode value.
         Java_Prompt_getLine函数调用 JNI函数GetStringUTFChars来读取字符串内容。GetStringUTFChars函数通过JNIEnv接口指针调用。它把jstring引用,通常在Java虚拟机实现(implementation)中表示为Unicode序列,转换为UTF-8格式的C语言字符串。如果你能确定初始的字符串仅包含7字节ASCII字符,那么你可以直接将转换过的字符串传递给常规C语言库函数比如printf。(我们将会在8.2节讨论怎么处理非ASCII字符串)
                   JNIEXPORT jstring JNICALL
                   Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
                   {
                            char buf[128];
                            const jbyte *str;
                            str = (*env)->GetStringUTFChars(env, prompt, NULL);
                            if (str == NULL) {
                                     return NULL; /* OutOfMemoryError already thrown */
                            }
                            printf("%s", str);
                            (*env)->ReleaseStringUTFChars(env, prompt, str);
                            /* We assume here that the user does not type more than
                            * 127 characters */
                            scanf("%s", buf);
                            return (*env)->NewStringUTF(env, buf);
                   }
        不要忘了检查GetStringUTFChars的返回值。因为Java虚拟机需要为UTF-8字符串分配内存,因此存在内存分配失败的风险。当这种情况发生时,GetStringUTFChars返回NULL并且抛出OutOfMemoryError异常。正如我们将要在第6章中学到的,通过JNI抛出异常与在Java中抛出异常时不同的。通过JNI抛出的pending异常(pending exception)不会自动改变native C代码中的控制流。相反,我们需要给出(issue)一个明确的返回值(return statement)以跳过C函数中遗留的返回值。当Java_Prompt_getLine返回后,异常将会在Prompt.mainPrompt.getLinenative方法的调用者中抛出。

3.2.2 释放native字符串资源

          当你的native代码使用完通过GetStringUTFChars获得的UTF-8字符串之后,它将调用ReleaseStringUTFChars。调用ReleaseStringUTFChars表示native方法不再需要GetStringUTFChars返回的字符串,因此UTF-8字符串占用的内存可以被释放。没有正确调用ReleaseStringUTFChars将会导致内存泄露,最终会导致内存耗尽

3.2.3 构造新字符串

         你可以通过调用JNI函数NewStringUTFnative方法中创建java.lang.String的实例。NewStringUTF取得(takes)一个UTF-8格式的C字符串,并创建一个java.lang.String实例。新创建的java.lang.String实例与给出的UTF-8格式的C 语言字符串具有相同的Unicode字符序列。
        如果虚拟机不能分配创建java.lang.String实例所需要的内存,NewStringUTF会抛出OutOfMemoryError异常并返回NULL.在这个示例中,我们不需要检查它的返回值,因为 native方法随即(immediately afterwardsreturn。如果 NewStringUTF执行失败,OutOfMemoryError异常将会在执行native方法调用的Prompt.main方法中抛出。如果NewStringUTF执行成功,它将返回一个指向新创建的java.lang.String实例的JNI引用。新实例通过Prompt.gerLine返回,然后赋给Prompt.main中的局部变量input

3.2.4 其他JNI函数

          除了之前介绍的GetStringUTFCharsReleaseStringUTFCharsNewStringUTF之外,JNI还支持大量其他的字符串相关函数。GetStringCharsReleaseStringChars获得Unicode格式的字符串。当操作系统支持Unicode作为native字符串格式时,这些函数是相当有用的。
         UTF-8字符串总是以'\0'结尾,而Unicode并非如此。JNI程序员可以调用GetStringLength获得(find outjstring 引用中Unicode字符的数目。为了知道表示(represent)一个UTF-8 格式的jstring需要多少字节。JNI程序员也可以在GetStringUTFChars返回结果的基础上调用ANSI C函数strlencall the ANSI C functionstrlenon the result of GetStringUTFChars),或者直接调用JNI函数GetStringUTFLength,传入jstring引用(call the JNI functionGetStringUTFLength on the jstringreference directly)。
         GetStringCharsGetStringUTFChars的第三个参数需要额外的声明(explanation):
                   const jchar *
                   GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);
       紧跟着GetStringChars返回之后,如果返回的字符串是原始的java.lang.String实例中字符串的副本(copy),那么isCopy指向的内存位置将会被置为JNI_TRUE。如果返回的字符串是一个指向原始的java.lang.String实例中字符串(characters)的直接(direct)指针,那么isCopy指向的内存位置将会被置为JNI_FALSE。如果isCopy指向的位置被置为JNI_FALSEnative 代码不能修改返回的字符串(characters)的内容。(Upon returning from GetStringChars, the memory location pointed to by isCopy will be set toJNI_TRUE if the returned string is a copy of the characters in the original java.lang.String instance. The memory location pointed to by isCopy will be set to JNI_FALSE if the returned string is a direct pointer to the characters in the original java.lang.String instance. When the location pointed to by isCopy is set to JNI_FALSE, native code must not modify the contents of the returned string.)违反这条规则会使得原始的 java.lang.String实例也被修改。这样就破坏(break)了java.lang.String实例不可改变的不变性(invariant)。
          最常见的情况是传递NULL作为isCopy参数,因为你根本不关心java虚拟机返回的是java.lang.String实例中字符串的副本还是直接指针指向的原始字符串。
 通常来说,预测虚拟机是否复制给出的java.lang.String实例中的字符串(characters)是不可能的。因此程序员必须假定GetStringChars这样的函数可以根据java.lang.String实例中的字符数量自动消耗相称的时间和空间。( Programmers must therefore assume functions such as GetStringChars may take time and space proportional to the number of characters in thejava.lang.String instance.)在经典的Java虚拟机实现中,垃圾回收器在堆中重新放置对象。一旦指向java.lang.String实例的直接指针被传回native代码中,垃圾回收器将不再重新配置java.lang.String实例。To put it another way, the virtual machine must pin the java.lang.String instance. Because excessive pinning leads to memory fragmentation, the virtual machine implementation may, at its discretion, decide to either copy the characters or pin the instance for each individual GetStringChars call.
      当你不再访问GetStringChars返回的字符串单元(element)时,不要忘记调用ReleaseStringChars。无论GetStringChars*isCopy设置为 JNI_TRUE还是JNI_FALSE,调用ReleaseStringChars都是必须的。根据GetStringChars返回的是一个copy还是一个direct指针,ReleaseStringChars也相应的释放 copyunpin 实例。

3.2.5 JDK 1.2中新的JNI字符串函数

        为增加虚拟机返回指向java.lang.String实例中字符串(characters)的direct指针的可能性,Java 2 SDK发行版1.2中增加了一对新函数Get/ReleaseStringCritical。表面上看,它们跟Get/ReleaseStringChars函数非常类似,如果可能的话都会返回一个指向字符串的指针,否则,创建一个副本(copy)。然而,对于怎么使用这些函数有着明确的限制。
          你必须认为(treat)这对函数内部的代码运行在临界区(critical region),native 代码不能调用任意的JNI函数或任何可能造成当前线程被锁住和等待运行在另一个虚拟机里的线程的native方法。例如,当前线程不能等待另一个线程的I/O流中的输入(the current thread must not wait for input on an I/O stream being written to by another thread.)。
          这些限制可能会导致这么一种情况:当native代码持有(hold)一个指向通过GetStringCritical获得的字符串单元的direct指针时,虚拟机的垃圾回收器可能会变为不可用状态。当垃圾回收器不可用时,任何启动(trigger)垃圾回收器的线程也将被锁住。Get/ReleaseStringCritical对中间的native代码必须不能造成阻塞调用(blocking call)或者在虚拟机中分配内存给一个新对象。否则,虚拟机将陷入停顿。考虑下列场景:
         1、被另一个线程启动( tigger)的垃圾回收器不能进行下一步( make progress)直到当前线程完成阻塞调用(blocking call)并使垃圾回收器重新变为可用状态。
         2、与此同时,当前线程不能进行下一步(make progress)因为阻塞调用(blocking call)需要获得一个早已被其他线程持有的锁,这个线程在等待使用垃圾回收器。
          嵌套使用(overlap multiple)多对GetStringCriticalReleaseStringCritical是安全的,例如:
                   jchar *s1, *s2;
                   s1 = (*env)->GetStringCritical(env, jstr1);
                   if (s1 == NULL) {
                            ... /* error handling */
                   }
                   s2 = (*env)->GetStringCritical(env, jstr2);
                   if (s2 == NULL) {
                            (*env)->ReleaseStringCritical(env, jstr1, s1);
                            ... /* error handling */
                   }
                   ... /* use s1 and s2 */
                   (*env)->ReleaseStringCritical(env, jstr1, s1);
                   (*env)->ReleaseStringCritical(env, jstr2, s2);
         Get/ReleaseStringCritical不必严格按照堆顺序嵌套使用(need not be strictly nested in a stack order)。我们绝不能忘记检查它的返回值以免因为内存不足而返回NULL的场景(We must not forget to check its return value against NULL for possible out of memory situations)。因为如果虚拟机(VM)内部用不同的格式表示数组,GetStringCritical可能会分配一个缓冲区然后做一个数组的copy。例如,Java虚拟机可能不会将数组连续存放(not store array contiguously)。这样的话,GetStringCritical必须赋值jstring实例中所有的字符以便于返回连续的字符数组给native代码。
          为避免死锁的出现,你必须确保native代码在它结束GetStringCritical之后和进行相应的ReleaseStringCritical调用之前部队任意(arbitrary)的JNI函数进行调用。在临界区(critical region)允许调用的函数只有嵌套的 Get/ReleaseStringCritical函数和Get/RealeasePrimitiveArrayCritical 
         JNI不支持GetStringUTFCritical函数的 ReleaseStringUTFCritical函数,这些函数可能会要求虚拟机对字符串进行复制,因为大部分虚拟机内部字符串都是Unicode格式。
         Java 2 SDK发行版1.2新增的还有GetStringRegionGetStringUTFRegion。这些函数把字符串单元拷到预分配的(preallocated)缓冲区中。Prompt.getLine方法可以使用GetStringUTFRegion重写:
                   JNIEXPORT jstring JNICALL
                   Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
                   {
                            /* assume the prompt string and user input has less than 128
                            characters */
                            char outbuf[128], inbuf[128];
                            int len = (*env)->GetStringLength(env, prompt);
                            (*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf);
                            printf("%s", outbuf);
                            scanf("%s", inbuf);
                            return (*env)->NewStringUTF(env, inbuf);
                   }
         GetStringUTFRegion函数接受一个起始索引和长度,都被算作Unicode字符数(both counted as number of Unicode characters)。此函数也会进行边界检查,如果必要的话会抛出StringIndexOutOfBoundsException。在上面的代码中,我们从字符串引用自身获得字符串长度因此确定没有索引溢出( index overflow)。(然而,上面的代码缺少必要的检查来确保 prompt字符串少于128个字符。)
          这个代码比使用GetStringUTFChars简单点。因为GetStringUTFRegion不进行内存分配,我们不需要检查可能的内存不足的情况。(另外,上面的代码缺少必要的检查来确保prompt字符串少于128个字符。)

3.2.6 JNI字符串函数总结

          3.1总结了所有JNI的字符串相关函数。Java 2 SDK 1.2发行版增加了大量新函数,提高了对特定字符串进行操作的性能。新增的函数不支持任何新的操作,除了带来性能的改善。

3.1 JNI字符串函数总结

       3.2.7 在字符串函数中进行选择

          3.2对程序员应该如何选择使用JDK1.1JDK 1.2中的字符串相关函数进行了说明:
          
          如果你想适用于1.11.11.2版本,除了Get/ReleaseStringCharsGet/ReleaseStringUTFChars没有其他的选择。
          如果你使用 JDK1.2或更高版本JDK ,并且你想将字符串内容复制到预分配好的 C缓冲区中,使用GetStringRegion GetStringUTFRegion      对于一些固定长度的短字符串, Get/SetStringRegionGet/SetStringUTFRegion 几乎总是应该被优先使用的函数,因为C语言缓冲区可以在C语言的栈中以非常小的代价进行分配(the C buffer can be allocated on the C stack very cheaply)。复制字符串中少量字符的开销(overhead)是可以忽略不计的。         Get/SetStringRegionGet/SetStringUTFRegion的一个优势是,他们不进行内存分配,因此不会抛出意料之外的内存不足异常。如果你确信索引溢出不会发生,就没有必要进行异常检查。Get/SetStringRegionGet/SetStringUTFRegion的另一个优势是你可以指明索引其实位置与字符数目。如果native代码只需要访问一个长字符串的子字符串时,这些函数式比较适合的。GetStringCritical使用的时候必须格外小心。你必须确定,当持有通过GetStringCritical得到的指针时,native代码不再Java虚拟机中为新的对象分配内存或执行其他可能造成系统死锁的阻塞调用。有一个实例来示范(demonstrate)在使用GetStringCritical时出现的微妙的问题。下面代码获取字符串内容然后调用fprint函数将字符串写到文件句柄fd中:
                   /* This is not safe! */
                   const char *c_str = (*env)->GetStringCritical(env, j_str, 0);
                   if (c_str == NULL) {
                            ... /* error handling */
                   }
                   fprintf(fd, "%s\n", c_str);
                   (*env)->ReleaseStringCritical(env, j_str, c_str);
       上面代码的问题在于,当垃圾回收器被当前线程禁用时,向一个文件句柄中写东西并不总是安全的。例如,假设有一个线程T正在等待读取fd文件句柄(file handle)。让我们进一步假设操作系统缓冲区这样设置:fprintf调用一直处于等待状态,直到线程T完成从fd中读取所有挂起数据的操作。我们建立了一个死锁的场景:如果线程T不能为从文件句柄中读取的数据分配足够的内存作为缓冲区的话,那么它必须请求使用垃圾回收器。然而,垃圾回收器请求将会被锁住(blocked)直到当前线程执行完毕ReleaseStringCritical,而这不可能在fprintf调用返回之前发生。然而,fprintf调用等待线程T结束从文件句柄中读取数据。
          下面的代码虽然与上面的代码比较相似,但是几乎可以肯定没有死锁:
                   /* This code segment is OK. */
                   const char *c_str = (*env)->GetStringCritical(env, j_str, 0);
                   if (c_str == NULL) {
                            ... /* error handling */
                   }
                   DrawString(c_str);
                   (*env)->ReleaseStringCritical(env, j_str, c_str);
         DrawString是一个系统调用,可以直接向屏幕上写字符串。除非屏幕显示驱动也是运行于同一个虚拟机的Java应用,否则DrawString不会因无限期的等待垃圾回收器而被锁住。
          总之,在Get/ReleaseStringCritical调用之间,你必须考虑所有锁的行为。

3.3 访问数组

         JNI对于基本数组(primitive array)和对象数组(object array)的处理是不同的。基本数组包含的元素是基本类型如intboolean。对象数组包含的元素是引用类型比如类的实例和其他数组。例如,在下面的Java代码段中,iarrfarr是基本数组,而oarrarr2是对象数组:
                   int[] iarr;
                   float[] farr;
                   Object[] oarr;
                   int[][] arr2;
          native代码中访问基本数组需要向访问字符串那样使用JNI函数。让我们来看一个简单的例子。下面程序调用了一个native方法sunArray 来把int数组中的元素相加:
                   class IntArray {
                            private native int sumArray(int[] arr);
                            public static void main(String[] args) {
                                     IntArray p = new IntArray();
                                     int arr[] = new int[10];
                                     for (int i = 0; i < 10; i++) {
                                               arr[i] = i;
                                     }
                                     int sum = p.sumArray(arr);
                                     System.out.println("sum = " + sum);
                            }
                            static {
                                     System.loadLibrary("IntArray");
                            }
                   }

3.3.1 C中访问数组

          数组用jarray类型和它的“子类型”比如jintarray表示。正如jstring不是C语言字符串类型一样,jarray类型也不是C语言数组类型。你不能间接通过jarray引用实现Java_IntArray_sumArray native方法。下面的C语言代码是非法的,将不会产生预期的结果:
                   /* This program is illegal! */
                   JNIEXPORT jint JNICALL
                   Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
                   {
                            int i, sum = 0;
                            for (i = 0; i < 10; i++) {
                                     sum += arr[i];
                            }
                   }
          正如下面实例所示,你必须使用合适的JNI函数来访问基本数组元素:
                   JNIEXPORT jint JNICALL
                   Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
                   {
                            jint buf[10];
                            jint i, sum = 0;
                            (*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
                            for (i = 0; i < 10; i++) {
                                     sum += buf[i];
                            }
                            return sum;
                   }

3.3.2 访问基本类型数组

         前面的例子使用GetIntArrayRegion函数复制int数组中所有的元素到一个C缓冲区(buf)中。第三个参数是元素开始时的索引(index),第四个参数是要复制的元素的数目。一旦元素被复制到C缓冲区后,我们就可以在native代码中访问他们。因为我们知道例子中数组长度是 10,因此不会出现index溢出,因此不必进行异常检查。
         JNI支持对应的SetIntArrayRegion函数,这个函数允许native代码修改int类型的数组元素。JNI也提供了对其他基本类型此操作的支持。
         JNI支持同源的(a family ofGet/Release<Type>ArrayElements函数(包括Get/ReleaseIntArrayElements,这些函数允许native代码获得一个指向基本数组元素的direct指针。因为底层的垃圾回收器可能不支持pinning,因此虚拟机可能会返回指向原始基本数组副本的指针。我们可以像下面这样用GetIntArrayElements重写3.3.1节中native方法的实现:
                   JNIEXPORT jint JNICALL
                   Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
                   {
                            jint *caar;
                            jint i, sum = 0;
                            carr = (*env)->GetIntArrayElements(env, arr, NULL);
                            if(carr == NULL){
                                     return 0;//exception occurred
                            }
                            for(i=0; i<10; i++){
                                     sum += carr[i];
                            }
                            (*env)->ReleaseIntArrayElements(env, arr, carr, 0);
                            return sum;
                   }
         GetArrayLength函数返回一个基本数组或对象数组中元素个数。当第一次给数组分配空间时,数组的长度就被确定了。
         JDK1.2引入了Get/ReleasePrimitiveArrarCritical函数。这些函数允许当native代码访问基本数组内容时虚拟机使垃圾回收机制不可用。程序员必须像使用Get/ReleaseStringCritical那样小心。在Get/ReleasePrimitiveArrayCritical函数之间,native代码不允许调用任意(arbitrary)的JNI函数,或者执行任何可能造成程序死锁的阻塞操作。

3.3.3 JNI基本数组函数总结

          
3.2是对所有与基本数组相关的JNI函数的总结。JDK 1.2增加了大量新函数,使对特定数组进行操作的性能大大提高。新增的函数除了性能的提升不支持新的操作。

3.3.4 在基本数组函数之间进行选择

          
3.3对程序员应该如何选择使用JDK1.1JDK 1.2中的JNI函数来访问基本数组进行了说明:

          如果你需要复制或从预分配的C缓冲区中复制,使用Get/Set<Type>ArrayRegion族函数(family of functions)。这些函数会执行边界检查并且如果必要会抛出ArrayIndexOutOfBoundsException异常。3.3.1节中的native方法使用 GetIntArrayRegionjarray引用中(out of a jarray reference)复制10个元素。对一些长度确定的小的数组,Get/Set<Type>ArrayRegion几乎总是最优先选择的函数,因为C语言缓冲区可以在C语言的栈中以非常小的代价进行分配。复制数组中少量元素的开销(overhead)是可以忽略不计的。
         Get/Set<Type>ArrayRegion函数允许你指明开始下标(index)和元素数目,因此如果native代码只需要范文一个大数组子集时,这是优先选择的函数。
          如果你没有预分配的C缓冲区,基本数组是不定长的并且当native代码持有指向数组元素的指针时,不会造成阻塞调用,那么可以使用JDK 1.2中的Get/ReleasePrimitiveArrayCritical函数。正如Get/ReleaseStringCritical函数一样,Get/ReleasePrimitiveArrayCritical使用时必须格外小心以避免死锁。
          使用Get/Release<type>ArrayElements族函数(family of function)总是安全的。虚拟机返回一个指向数组元素的direct指针或返回一个保存有数组元素副本的缓冲区(buffer)。

3.3.5 访问对象数组

         JNI提供了一对单独的函数来访问对象数组,GetObjectArrayElement返回指定下标(index)处的元素,而SetObjectArrayElement更新指定下标(index)处的元素。不像基本数组类型那样,你不能获取所有的对象元素或者一次同时复制多个元素。
          字符串和数组都是引用类型。你可以使用Get/SetObjectArrayElement访问字符串数组和高维数组(array of arrays)。
          下面的例子调用native方法创建一个二维int数组然后打印数组内容:
                   class ObjectArrayTest {
                            private static native int[][] initInt2DArray(int size);
                            public static void main(String[] args) {
                                     int[][] i2arr = initInt2DArray(3);
                                     for (int i = 0; i < 3; i++) {
                                               for (int j = 0; j < 3; j++) {
                                                        System.out.print(" " + i2arr[i][j]);
                                               }
                                               System.out.println();
                                     }
                            }
                            static {
                                     System.loadLibrary("ObjectArrayTest");
                            }
                   }
          静态native方法initInt2DArray创建一个给定尺寸的二维数组。为二维数组分配内存并初始化的native方法可以向下面这样写:
                   JNIEXPORT jobjectArray
                   JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size)
                   {
                            jobjectArray result;
                            int i;
                            jclass intArrCls = (*env)->FindClass(env, "[I");
                            if(intArrCls == NULL){
                                     return NULL;//exception thrown
                            }
                            result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
                            if(result == NULL){
                                     return NULL;//out of memory error thrown
                            }
                            for(i = 0; i < size; i++){
                                     jint tmp[256]; //make sure it is large enough!
                                     int j;
                                     jintArray iarr = (*env)->NewIntArray(env, size);
                                     if(iarr == NULL){
                                               return NULL;//out of memory error thrown
                                     }
                                     for(j = 0; j < size; j++) {
                                               tmp[j] = i + j;
                                     }
                                     (*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
                                     (*env)->SetObjectArrayElement(env, result, i, iarr);
                                     (*env)->DeleteLocalRef(env, iarr);
                            }
                            return result;
                   }
         newInt2DArray(?Java_ObjectArrayTest_initInt2DArray)方法首先调用JNI函数FindClass来获得一个指向二维int数组中元素类的引用(a reference of the element class of the two-dimensionalintarray)。FindClass的“[I”参数是JNI类说明符(class descriptor),对应的是Java中的int[]类型。如果类加载失败,FIndClass返回NULL并抛出异常(可能由于丢失类文件或内存不足)。
          然后NewObjectArray函数为数组分配内存,这个数组的元素类型为intArrCls类引用(denoted by the intArrCls class reference)。NewObjectArray只负责分配第一维的尺寸(acllocate the fiest dimension),我们仍然有一个剩余的任务,那就是填充数组元素构成第二维。Java虚拟机没有特别的数据结构供多维数组使用。二维数组是简单的“数组的数组”(array of arrays)。
          创建第二维的代码相当简单。NewIntArray为每个数组元素分配内存,SetIntArrayRegion复制tmp[]缓冲区中内容放到新的一维数组中。完成对SetObjectArrayElement的调用之后,第i个一维数组的第 j个元素值为i+j 
          运行ObjectArrayTest.main方法产生如下输出:
                   0 1 2
                   1 2 3
                   2 3 4
          在循环结束时调用的DeleteLocalRef确保虚拟机不会由于持有像iarr这样的JNI引用。5.2.1节当中详细解释了什么时候以及为什么需要调用 DeleteLocalRef
0 0