在第6章我們學習到了在Native層如何調用Java靜態方法和實例方法,其中調用實例方法的示例代碼中也提到了調用構造函數來實始化一個對象,但沒有詳細介紹,一帶而過了。還沒有閱讀過的同學請移步《[JNI/NDK開發指南(六)——C/C++訪問Java實例方法和靜態方法](http://blog.csdn.net/xyang81/article/details/42582213)》閱讀。這章詳細來介紹下初始一個對象的兩種方式,以及如何調用子類對象重寫的父類實例方法。
我們先回過一下,在Java中實例化一個對象和調用父類實例方法的流程。先看一段代碼:
~~~
package com.study.jnilearn;
public class Animal {
public void run() {
System.out.println("Animal.run...");
}
}
package com.study.jnilearn;
public class Cat extends Animal {
@Override
public void run() {
System.out.println(name + " Cat.run...");
}
}
public static void main(String[] args) {
Animal cat = new Cat("湯姆");
cat.run();
}
~~~
正如你所看到的那樣,上面這段代碼非常簡單,有兩個類Animal和Cat,Animal類中定義了run和getName兩個方法,Cat繼承自Animal,并重寫了父類的run方法。在main方法中,首先定義了一個Animal類型的變量cat,并指向了Cat類的實例對象,然后調用了它的run方法。***在執行new Cat(“湯姆”)這段代碼時,會先為Cat類分配內存空間(所分配的內存空間大小由Cat類的成員變量數量決定),然后調用Cat的帶參構造方法初始化對象。***cat是Animal類型,但它指向的是Cat實例對象的引用,而且Cat重寫了父類的run方法,因為調用run方法時有多態存在,所以訪問的是Cat的run而非Animal的run,運行后打印的結果為:***湯姆 Cat.run…***
如果要調用父類的run方法,只需在Cat的run方法中調用super.run()即可,相當的簡單。
> 寫過C或C++的同學應該都有一個很深刻的內存管理概念,**棧空間和堆空間**,棧空間的內存大小受操作系統限制,由操作系統自動來管理,速度較快,所以在函數中定義的局部變量、函數形參變量都存儲在棧空間。操作系統沒有限制堆空間的內存大小,只受物理內存的限制,內存需要程序員自己管理。在C語言中用malloc關鍵字動態分配的內存和在C++中用new創建的對象所分配內存都存儲在堆空間,內存使用完之后分別用free或delete/delete[]釋放。*這里不過多的討論C/C++內存管理方面的知識,有興趣的同學請自行百度。*做Java的童鞋眾所周知,寫Java程序是不需要手動來管理內存的,內存管理那些煩鎖的事情全都交由一個叫GC的線程來管理(當一個對象沒有被其它對象所引用時,該對象就會被GC釋放)。但我覺得Java內部的內存管理原理和C/C++是非常相似的,上例中,Animal cat = new Cat(“湯姆”); 局部變量cat存放在棧空間上,new Cat(“湯姆”);創建的實例對象存放在堆空間,返回一個內存地址的引用,存儲在cat變量中。這樣就可以通過cat變量所指向的引用訪問Cat實例當中所有可見的成員了。
所以創建一個對象分為2步:
1. 為對象分配內存空間
2. 初始化對象(調用對象的構造方法)
下面通過一個示例來了解在JNI中是如何調用對象構造方法和父類實例方法的。為了讓示例能清晰的體現構造方法和父類實例方法的調用流程,定義了Animal和Cat兩個類,Animal定義了一個String形參的構造方法,一個成員變量name、兩個成員函數run和getName,Cat繼承自Animal,并重寫了run方法。在JNI中實現創建Cat對象的實例,調用Animal類的run和getName方法。代碼如下所示:
~~~
// Animal.java
package com.study.jnilearn;
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal Construct call...");
}
public String getName() {
System.out.println("Animal.getName Call...");
return this.name;
}
public void run() {
System.out.println("Animal.run...");
}
}
// Cat.java
package com.study.jnilearn;
public class Cat extends Animal {
public Cat(String name) {
super(name);
System.out.println("Cat Construct call....");
}
@Override
public String getName() {
return "My name is " + this.name;
}
@Override
public void run() {
System.out.println(name + " Cat.run...");
}
}
// AccessSuperMethod.java
package com.study.jnilearn;
public class AccessSuperMethod {
public native static void callSuperInstanceMethod();
public static void main(String[] args) {
callSuperInstanceMethod();
}
static {
System.loadLibrary("AccessSuperMethod");
}
}
~~~
AccessSuperMethod類是程序的入口,其中定義了一個native方法callSuperInstanceMethod。用javah生成的jni函數原型如下:
~~~
/* Header for class com_study_jnilearn_AccessSuperMethod */
#ifndef _Included_com_study_jnilearn_AccessSuperMethod
#define _Included_com_study_jnilearn_AccessSuperMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_study_jnilearn_AccessSuperMethod
* Method: callSuperInstanceMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
~~~
實現Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod函數,如下所示:
~~~
// AccessSuperMethod.c
#include "com_study_jnilearn_AccessSuperMethod.h"
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod
(JNIEnv *env, jclass cls)
{
jclass cls_cat;
jclass cls_animal;
jmethodID mid_cat_init;
jmethodID mid_run;
jmethodID mid_getName;
jstring c_str_name;
jobject obj_cat;
const char *name = NULL;
// 1、獲取Cat類的class引用
cls_cat = (*env)->FindClass(env, "com/study/jnilearn/Cat");
if (cls_cat == NULL) {
return;
}
// 2、獲取Cat的構造方法ID(構造方法的名統一為:<init>)
mid_cat_init = (*env)->GetMethodID(env, cls_cat, "<init>", "(Ljava/lang/String;)V");
if (mid_cat_init == NULL) {
return; // 沒有找到只有一個參數為String的構造方法
}
// 3、創建一個String對象,作為構造方法的參數
c_str_name = (*env)->NewStringUTF(env, "湯姆貓");
if (c_str_name == NULL) {
return; // 創建字符串失敗(內存不夠)
}
// 4、創建Cat對象的實例(調用對象的構造方法并初始化對象)
obj_cat = (*env)->NewObject(env,cls_cat, mid_cat_init,c_str_name);
if (obj_cat == NULL) {
return;
}
//-------------- 5、調用Cat父類Animal的run和getName方法 --------------
cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal");
if (cls_animal == NULL) {
return;
}
// 例1: 調用父類的run方法
mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 獲取父類Animal中run方法的id
if (mid_run == NULL) {
return;
}
// 注意:obj_cat是Cat的實例,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID
(*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run);
// 例2:調用父類的getName方法
// 獲取父類Animal中getName方法的id
mid_getName = (*env)->GetMethodID(env, cls_animal, "getName", "()Ljava/lang/String;");
if (mid_getName == NULL) {
return;
}
c_str_name = (*env)->CallNonvirtualObjectMethod(env, obj_cat, cls_animal, mid_getName);
name = (*env)->GetStringUTFChars(env, c_str_name, NULL);
printf("In C: Animal Name is %s\n", name);
// 釋放從java層獲取到的字符串所分配的內存
(*env)->ReleaseStringUTFChars(env, c_str_name, name);
quit:
// 刪除局部引用(jobject或jobject的子類才屬于引用變量),允許VM釋放被局部變量所引用的資源
(*env)->DeleteLocalRef(env, cls_cat);
(*env)->DeleteLocalRef(env, cls_animal);
(*env)->DeleteLocalRef(env, c_str_name);
(*env)->DeleteLocalRef(env, obj_cat);
}
~~~
### 運行結果:

### 代碼講解 - 調用構造方法
調用構造方法和調用對象的實例方法方式是相似的,傳入”< init >”作為方法名查找類的構造方法ID,然后調用JNI函數NewObject調用對象的構造函數初始化對象。如下代碼所示:
~~~
obj_cat = (*env)->NewObject(env,cls_cat,mid_cat_init,c_str_name);
~~~
上述這段代碼調用了JNI函數NewObject創建了Class引用的一個實例對象。這個函數做了2件事情,1> 創建一個未初始化的對象并分配內存空間 2> 調用對象的構造函數初始化對象。這兩步也可以分開進行,為對象分配內存,然后再初始化對象,如下代碼所示:
~~~
// 1、創建一個未初始化的對象,并分配內存
obj_cat = (*env)->AllocObject(env, cls_cat);
if (obj_cat) {
// 2、調用對象的構造函數初始化對象
(*env)->CallNonvirtualVoidMethod(env,obj_cat, cls_cat, mid_cat_init, c_str_name);
if ((*env)->ExceptionCheck(env)) { // 檢查異常
goto quit;
}
}
~~~
AllocObject函數創建的是一個未初始化的對象,后面在用這個對象之前,必須調用CallNonvirtualVoidMethod調用對象的構造函數初始化該對象。而且在使用時一定要非常小心,確保在一個對象上面,構造函數最多被調用一次。有時,先創建一個初始化的對象,然后在合適的時間再調用構造函數的方式是很有用的。盡管如此,大部分情況下,應該使用 NewObject,盡量避免使用容易出錯的AllocObject/CallNonvirtualVoidMethod函數。
### 代碼講解 - 調用父類實例方法
如果一個方法被定義在父類中,在子類中被覆蓋,也可以調用父類中的這個實例方法。JNI 提供了一系列函數CallNonvirtualXXXMethod來支持調用各種返回值類型的實例方法。調用一個定義在父類中的實例方法,須遵循下面的步驟:
1.使用GetMethodID函數從一個指向父類的Class引用當中獲取方法ID
~~~
cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal");
if (cls_animal == NULL) {
return;
}
//例1: 調用父類的run方法
mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 獲取父類Animal中run方法的id
if (mid_run == NULL) {
return;
}
~~~
2.傳入子類對象、父類Class引用、父類方法 ID 和參數,并調用 CallNonvirtualVoidMethod、
CallNonvirtualBooleanMethod、CallNonvirtualIntMethod等一系列函數中的一個。其中CallNonvirtualVoidMethod 也可以被用來調用父類的構造函數。
~~~
// 注意:obj_cat是Cat的實例,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID
(*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run);
~~~
其實在開發當中,這種調用父類實例方法的情況是很少遇到的,通常在 JAVA 中可以很簡單地做到: super.func();但有些特殊需求也可能會用到,所以知道有這么回事還是很有必要的。
*[示例代碼下載地址:https://code.csdn.net/xyang81/jnilearn](https://code.csdn.net/xyang81/jnilearn)*
- 前言
- JNI/NDK開發指南(開山篇)
- JNI/NDK開發指南(一)—— JNI開發流程及HelloWorld
- JNI/NDK開發指南(二)——JVM查找java native方法的規則
- JNI/NDK開發指南(三)——JNI數據類型及與Java數據類型的映射關系
- JNI/NDK開發指南(四)——字符串處理
- Android NDK開發Crash錯誤定位
- JNI/NDK開發指南(五)——訪問數組(基本類型數組與對象數組)
- JNI/NDK開發指南(六)——C/C++訪問Java實例方法和靜態方法
- JNI/NDK開發指南(七)——C/C++訪問Java實例變量和靜態變量
- JNI/NDK開發指南(八)——調用構造方法和父類實例方法
- JNI/NDK開發指南(九)——JNI調用性能測試及優化
- JNI/NDK開發指南(十)——JNI局部引用、全局引用和弱全局引用
- Android JNI局部引用表溢出:local reference table overflow (max=512)
- JNI/NDK開發指南(十一)——JNI異常處理