JNI/NDK开发指南(八)——调用构造方法和父类实例方法

转载请注明出处:http://blog.csdn.net/xyang81/article/details/44002089

在第6章我们学习到了在Native层如何调用Java静态方法和实例方法,其中调用实例方法的示例代码中也提到了调用构造函数来实始化一个对象,但没有详细介绍,一带而过了。还没有阅读过的同学请移步《JNI/NDK开发指南(六)——C/C++访问Java实例方法和静态方法》阅读。这章详细来介绍下初始一个对象的两种方式,以及如何调用子类对象重写的父类实例方法。

我们先回过一下,在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

时间: 2024-09-16 03:50:54

JNI/NDK开发指南(八)——调用构造方法和父类实例方法的相关文章

JNI/NDK开发指南(五)——访问数组(基本类型数组与对象数组)

          转载请注明出处:http://blog.csdn.net/xyang81/article/details/42346165          JNI中的数组分为基本类型数组和对象数组,它们的处理方式是不一样的,基本类型数组中的所有元素都是JNI的基本数据类型,可以直接访问.而对象数组中的所有元素是一个类的实例或其它数组的引用,和字符串操作一样,不能直接访问Java传递给JNI层的数组,必须选择合适的JNI函数来访问和设置Java层的数组对象.阅读此文假设你已经了解了JNI与J

JNI/NDK开发指南(开山篇)

      转载请注明出处:http://blog.csdn.net/xyang81/article/details/41759643        相信很多做过Java或Android开发的朋友经常会接触到JNI方面的技术,由其做过Android的朋友,为了应用的安全性,会将一些复杂的逻辑和算法通过本地代码(C或C++)来实现,然后打包成so动态库文件,并提供Java接口供应用层调用,这么做的目的主要就是为了提供应用的安全性,防止被反编译后被不法分子分析应用的逻辑.当然打包成so也不能说完全安

JNI/NDK开发指南(一)—— JNI开发流程及HelloWorld

                转载请注明出处:http://blog.csdn.net/xyang81/article/details/41777471        JNI全称是Java Native Interface(Java本地接口)单词首字母的缩写,本地接口就是指用C和C++开发的接口.由于JNI是JVM规范中的一部份,因此可以将我们写的JNI程序在任何实现了JNI规范的Java虚拟机中运行.同时,这个特性使我们可以复用以前用C/C++写的大量代码.        开发JNI程序会受

JNI/NDK开发指南(二)——JVM查找java native方法的规则

        转载请注明出处:http://blog.csdn.net/xyang81/article/details/41854185         通过第一篇文章,大家明白了调用native方法之前,首先要调用System.loadLibrary接口加载一个实现了native方法的动态库才能正常访问,否则就会抛出java.lang.UnsatisfiedLinkError异常,找不到XX方法的提示.现在我们想想,在Java中调用某个native方法时,JVM是通过什么方式,能正确的找到动

JNI/NDK开发指南(六)——C/C++访问Java实例方法和静态方法

        转载请注明出处:http://blog.csdn.net/xyang81/article/details/42582213         通过前面5章的学习,我们知道了如何通过JNI函数来访问JVM中的基本数据类型.字符串和数组这些数据类型.下一步我们来学习本地代码如何与JVM中任意对象的属性和方法进行交互.比如本地代码调用Java层某个对象的方法或属性,也就是通常我们所说的来自C/C++层本地函数的callback(回调).这个知识点分2篇文章分别介绍,本篇先介绍方法回调,在

JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用

转载请注明出处:http://blog.csdn.net/xyang81/article/details/44657385     这篇文章比较偏理论,详细介绍了在编写本地代码时三种引用的使用场景和注意事项.可能看起来有点枯燥,但引用是在JNI中最容易出错的一个点,如果使用不当,容易使程序造成内存溢出,程序崩溃等现象.所以讲得比较细,有些地方看起来可能比较啰嗦,还请轻啪!<Android JNI局部引用表溢出:local reference table overflow (max=512)>这

JNI/NDK开发指南(十一)——JNI异常处理

转载请注明出处:http://blog.csdn.net/xyang81/article/details/45770551 异常简介 异常,显而意见就是程序在运行期间没有按照正常的程序逻辑执行,在执行过程当中出现了某种错误,导致程序崩溃.在Java中异常分为运行时异常(RuntimeException)和编译时异常,在程序中有可能运行期间发生异常的逻辑我们会用try-catch-来处理,如果没有处理的话,在运行期间发生异常就会导致程序奔溃.而编译时异常是在编译期间就必须处理的.本章主要介绍运行时

JNI/NDK开发指南(七)——C/C++访问Java实例变量和静态变量

       转载请注明出处:http://blog.csdn.net/xyang81/article/details/42836783        在上一章中我们学习到了如何在本地代码中访问任意Java类中的静态方法和实例方法,本章我们也通过一个示例来学习Java中的实例变量和静态变量,在本地代码中如何来访问和修改.静态变量也称为类变量(属性),在所有实例对象中共享同一份数据,可以直接通过[类名.变量名]来访问.实例变量也称为成员变量(属性),每个实例都拥有一份实例变量数据的拷贝,它们之间修

JNI/NDK开发指南(三)——JNI数据类型及与Java数据类型的映射关系

        转载请注明出处:http://blog.csdn.net/xyang81/article/details/42047899        当我们在调用一个Java native方法的时候,方法中的参数是如何传递给C/C++本地函数中的呢?Java方法中的参数与C/C++函数中的参数,它们之间是怎么转换的呢?我猜你应该也有相关的疑虑吧,咱们先来看一个例子,还是以HelloWorld为例: HelloWorld.java: package com.study.jnilearn; cl