Опубликован: 04.12.2009 | Доступ: свободный | Студентов: 8337 / 610 | Оценка: 4.30 / 3.87 | Длительность: 27:27:00
Лекция 9:

Дополнительные элементы объектного программирования на языке Java

< Лекция 8 || Лекция 9: 123 || Лекция 10 >

9.2. Подключение внешних библиотек DLL."Родные" (native) методы*

*- данный параграф приводится в ознакомительных целях

Для прикладного программирования средств Java в подавляющем большинстве случаев хватает. Однако иногда возникает необходимость подключить к программе ряд системных вызовов. Либо обеспечить доступ к библиотекам, написанным на других языках программирования. Для таких целей в Java используются методы, объявленные с модификатором native – "родной". Это слово означает, что при выполнении метода производится вызов "родного" для конкретной платформы двоичного кода, а не платформо-независимого байт-кода как во всех других случаях. Заголовок "родного" метода описывается в классе Java, а его реализация осуществляется на каком-либо из языков программирования, позволяющих создавать динамически подключаемые библиотеки (DLLDynamic Link Library под Windows, Shared Objects под UNIX-образными операционными системами).

Правило для объявления и реализации таких методов носит название JNIJava Native Interface.

Объявление "родного" метода в Java имеет вид

Модификаторы native ВозвращаемыйТип имяМетода(список параметров);

Тело "родного" метода не задается – оно является внешним и загружается в память компьютера с помощью загрузки той библиотеки, из которой этот метод должен вызываться:

System.loadLibrary("ИмяБиблиотеки");

При этом имя библиотеки задается без пути и без расширения. Например, если под Windows библиотека имеет имя myLib.dll, или под UNIX или Linux имеет имя myLib.so, надо указывать System.loadLibrary("myLib") ;

В случае, если файла не найдено, возбуждается непроверяемая исключительная ситуация UnsatisfiedLinkError.

Если требуется указать имя библиотеки с путем, применяется вызов

System.load ("ИмяБиблиотекиСПутем");

Который во всем остальном абсолютно аналогичен вызову loadLibrary.

После того, как библиотека загружена, с точки зрения использования в программе вызов "родного" метода ничем не отличается от вызова любого другого метода.

Для создания библиотеки с методами, предназначенными для работы в качестве "родных", обычно используется язык С++. В JDK существует утилита javah.exe, предназначенная для создания заголовков C++ из скомпилированных классов Java. Покажем, как ей пользоваться, на примере класса ClassWithNativeMethod. Зададим его в пакете нашего приложения:

package java_example_pkg;
public class ClassWithNativeMethod {

    /** Creates a new instance of ClassWithNativeMethod */
    public ClassWithNativeMethod() {
    }
    
  public native void myNativeMethod();
}

Для того, чтобы воспользоваться утилитой javah, скомпилируем проект и перейдем в папку build\classes. В ней будут располагаться папка с пакетом нашего приложения java_example_pkg и папка META-INF. В режиме командной строки выполним команду

javah.exe java_example_pkg.ClassWithNativeMethod

- задавать имя класса необходимо с полной квалификацией, то есть с указанием перед ним имени пакета. В результате в папке появится файл java_example_pkg_ClassWithNativeMethod.h со следующим содержимым:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class java_example_pkg_ClassWithNativeMethod */

#ifndef _Included_java_example_pkg_ClassWithNativeMethod
#define _Included_java_example_pkg_ClassWithNativeMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: java_example_pkg_ClassWithNativeMethod
 * Method: myNativeMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL
Java_java_1example_1pkg_ClassWithNativeMethod_myNativeMethod
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Функция Java_java_1example_1pkg_ClassWithNativeMethod_myNativeMethod(JNIEnv *, jobject), написанная на C++, должна обеспечивать реализацию метода myNativeMethod() в классе Java. Имя функции C++ состоит из: префикса Java, разделителя "_", модифицированного имени пакета (знаки подчеркивания "_" заменяются на "_1"), разделителя "_", имени класса, разделителя "_", имени "родного" метода. Первый параметр JNIEnv * в функции C++ обеспечивает доступ "родного" кода к параметрам и объектам, передающимся из функции C++ в Java. В частности, для доступа к стеку. Второй параметр, jobject, – ссылка на экземпляр класса, в котором задан "родной" метод, для методов объекта, и jclassссылка на сам класс – для методов класса. В языке C++ нет ссылок, но в Java все переменные объектного типа являются ссылками. Соответственно, второй параметр отождествляется с этой переменной.

Если в "родном" методе имеются параметры, то список параметров функции C++ расширяется. Например, если мы зададим метод

public native int myNativeMethod(int i);

то список параметров функции C++ станет

(JNIEnv *, jobject, jint)

А тип функции станет jint вместо void.

Таблица 9.1. Соответствие типов Java и C++
Тип Java Тип JNI (C++) Характеристика типа JNI
boolean jboolean 1 байт, беззнаковый
byte jbyte 1 байт
char jchar 2 байта, беззнаковый
short jshort 2 байта
int jint 4 байта
long jlong 8 байт
float jfloat 4 байта
double jdouble 8 байт
void void -
Object jobject Базовый для остальных классов
Class jclass Ссылка на класс Java
String jstring Строки Java
массив jarray Базовый для классов массивов
Object[] jobjectArray Массив объектов
boolean[] jbooleanArray Массив булевских значений
byte[] jbyteArray Массив байт (знаковых значений длиной в байт)
char[] jcharArray Массив кодов символов
short[] jshortArray Массив коротких целых
int[] jintArray Массив целых
long[] jlongArray Массив длинных целых
float[] jfloatArray Массив значений float
double[] jdoubleArray Массив значений double
Throwable jthrowable Обработчик исключительных ситуаций

В реализации метода требуется объявить переменные. Например, если мы будем вычислять квадрат переданного в метод значения и возвращать в качестве результата значение параметра, возведенное в квадрат (пример чисто учебный), код реализации функции на C++ будет выглядеть так:

#include "java_example_pkg_ClassWithNativeMethod.h"
JNIEXPORT jint JNICALL
Java_java_1example_1pkg_ClassWithNativeMethod_myNativeMethod
  (JNIEnv *env, jobject obj, jint i ){
    return i*i
};

Отметим, что при работе со строками и массивами для получения и передачи параметров требуется использовать переменную env. Например, получение длины целого массива, переданного в переменную jintArray intArr, будет выглядеть так:

jsize length=(*env)->GetArrayLength(env, intArr);

Выделение памяти под переданный массив:

jint *intArrRef=(*env)->GetIntArrayElements(env, intArr,0);

Далее с массивом intArr можно работать как с обычным массивом C++. Высвобождение памяти из-под массива:

(*env)->ReleaseIntArrayElements(env, intArr, intArrRef ,0);

Имеются аналогичные функции для доступа к элементам массивов всех примитивных типов: GetBooleanArrayElements, GetByteArrayElements,…, GetDoubleArrayElements. Эти функции копируют содержимое массивов Java в новую область памяти, с которой и идет работа в C++. Для массивов объектов имеется не только функция GetObjectArrayElement, но и SetObjectArrayElement – для получения и изменения отдельных элементов таких массивов.

Строка Java jstring s преобразуется в массив символов C++ так:

const char *sRef=(*env)->GetStringUTFChars(env,s,0);

Ее длина находится как int s_len=strlen(sRef) ;

Высвобождается из памяти как

(*env)->ReleaseStringUTFChars(env,s,sRef);

Краткие итоги

  • Программу, выполняющуюся под управлением операционной системы, называют процессом ( process ), или, что то же, приложением. У каждого процесса свое адресное пространство. Потоки выполнения ( threads ) отличаются от процессов тем, что выполняются в адресном пространстве своего родительского процесса. Потоки выполняются параллельно (псевдопараллельно), но, в отличие от процессов, легко могут обмениваться данными в пределах общего виртуального адресного пространства. То есть у них могут иметься общие переменные, в том числе – массивы и объекты.
  • В приложении всегда имеется главный (основной) поток. Если он закрывается – закрываются все остальные пользовательские потоки приложения. Кроме них возможно создание потоков-демонов, которые могут продолжать работу и после окончания работы главного потока.
  • Любая программа Java неявно использует потоки. В главном потоке виртуальная Java-машина (JVM) запускает метод main приложения, а также все методы, вызываемые из него. Главному потоку автоматически дается имя "main".
  • Если разные потоки получают доступ к одним и тем же данным, причем один из них или они оба меняют эти данные, для них требуется обеспечить установить разграничение доступа. Пока один поток меняет данные, второй не должен иметь права их читать или менять. Он должен дожидаться окончания доступа к данным первого потока. Говорят, что осуществляется синхронизация потоков. В Java для этих целей служит оператор synchronize ("синхронизировать"). Иногда синхронизованную область кода (метод или оператор) называют критической секцией кода.
  • При запуске синхронизованного метода говорят, что объект входит в монитор, при завершении – что объект выходит из монитора. При этом поток, внутри которого вызван синхронизованный метод, считается владельцем данного монитора.
  • Имеется два способа синхронизации по ресурсам: синхронизация объекта и синхронизация метода.

    Синхронизация объекта obj1 при вызове несинхронизованного метода:

    synchronized(obj1) оператор;
    Синхронизация метода с помощью модификатора synchronized при задании класса:
    public synchronized тип метод(...){...}
  • Кроме синхронизации по данным имеется синхронизация по событиям, когда параллельно выполняющиеся потоки приостанавливаются вплоть до наступления некоторого события, о котором им сигнализирует другой поток. Основными операциями при таком типе синхронизации являются wait ("ждать") и notify ("оповестить").
  • Имеется два способа создать класс, экземплярами которого будут потоки: унаследовать класс от java.lang.Thread либо реализовать интерфейс java.lang.Runnable.
  • Интерфейс java.lang.Runnable имеет декларацию единственного метода public void run(), который обеспечивает последовательность действий при работе потока. Класс Thread уже реализует интерфейс Runnable, но с пустой реализацией метода run(). Поэтому в потомке Thread надо переопределить метод run().
  • При работе с большим количеством потоков требуется их объединение в группы. Такая возможность инкапсулируется классом TreadGroup ("Группа потоков").
  • Для получения доступа к библиотекам, написанным на других языках программирования, в Java используются методы, объявленные с модификатором native – "родной". При выполнении такого метода производится вызов "родного" для конкретной платформы двоичного кода, а не платформо-независимого байт-кода как во всех других случаях. Заголовок "родного" метода описывается в классе Java, а его реализация осуществляется на каком-либо из языков программирования, позволяющих создавать динамически подключаемые библиотеки.

Задания

  • Написать приложение, в котором используются потоки. Использовать задание класса потока как наследника Thread. Класс потока должен обеспечивать в методе run построчный несинхронизированный вывод в консольное окно чисел от 1 до 100 порциями по 10 чисел в строке, разделенных пробелами, причем перед каждой такой порцией должна стоять надпись "Thread 1:" для первого потока, "Thread 2:" для второго, и т.д. Для вывода строки задать в классе метод print10. В приложении по нажатии на первую кнопку должны создаваться два или более потоков, а при нажатии на вторую они должны стартовать.
  • Усовершенствовать приложение, обеспечив синхронизацию за счет объявления вызова print10 в методе run синхронизированным.
  • Создать копию класса потока, отличающуюся от первоначальной тем, что выводятся числа от 101 до 200, класс задан как реализующий интерфейс Runnable, а метод print10 задан как синхронизированный. Добавить в приложение создание и старт потоков – экземпляров данного класса.
< Лекция 8 || Лекция 9: 123 || Лекция 10 >
Максим Старостин
Максим Старостин

Код с перемещением фигур не стирает старую фигуру, а просто рисует новую в новом месте. Точку, круг.