Модификатор native для объявления методов
Код компилируется без единого замечания: метод объявлен, вызов на месте, IDE не подчёркивает ничего красным. А на запуске программа падает: java.lang.UnsatisfiedLinkError: 'int Demo.sum(int, int)'. Так выглядит первое знакомство с модификатором native: компилятор верит вам на слово, что реализация метода где-то существует, и проверяет это только во время выполнения — в момент первого вызова.
Что делает модификатор native
native — это модификатор метода в Java, который сообщает, что тело метода реализовано вне JVM: в скомпилированной библиотеке (.dll, .so, .dylib), написанной на языке с C-совместимым ABI — чаще всего на C или C++, но подойдут и Rust, Fortran, ассемблер. Java-код содержит только сигнатуру, тела нет, объявление заканчивается точкой с запятой.
public native int hashCode(); Ключевое слово native ничего не компилирует и не подключает само по себе. Оно решает ровно одну задачу: разрешает объявить метод без тела и помечает его как «реализация придёт извне». Связыванием Java-объявления с настоящей функцией занимается уже JNI (Java Native Interface) во время выполнения.
Зачем это нужно на практике:
- доступ к возможностям операционной системы, которых нет в стандартной библиотеке Java (низкоуровневая работа с устройствами, специфичные системные вызовы);
- переиспользование готовых C/C++-библиотек, которые нет смысла переписывать (кодеки, криптография, драйверы, движки машинного обучения);
- участки, критичные к производительности, где нужны SIMD-инструкции или ручное управление памятью;
- внутренняя кухня самой JDK — часть методов
Object,System,Threadпросто не может быть выражена на Java.
Важно
Слово native не означает «быстрее». Каждый переход из Java в нативный код и обратно стоит дорого: JVM фиксирует состояние потока, теряет часть оптимизаций JIT, а объекты приходится передавать через JNI-функции. Мелкий метод, вызываемый в цикле, после переноса в C нередко работает медленнее, чем его Java-версия.
JNI: как Java находит реализацию
JNI — это стандартный интерфейс между JVM и нативным кодом. Чтобы вызов native-метода сработал, должны совпасть три вещи.
- Библиотека загружена. Обычно это делают в статическом инициализаторе класса:
System.loadLibrary("demo")(ищет библиотеку вjava.library.path) илиSystem.load("/абсолютный/путь/libdemo.so"). - Имя функции совпадает. JVM ищет символ по строгому соглашению:
Java_+ пакет с точками, заменёнными на подчёркивания +_+ имя класса +_+ имя метода. Дляcom.example.Demo.sum()этоJava_com_example_Demo_sum. - Сигнатура совпадает. Первым параметром C-функции всегда идёт
JNIEnv*, вторым —jobject(для обычного метода) илиjclass(дляstatic), дальше — параметры самого метода в JNI-типах (jint,jlong,jstring,jobjectArray…).
Вручную имена писать не нужно — заголовочный файл генерирует компилятор:
javac -h . Demo.java Флаг -h появился у javac в JDK 8. Отдельная утилита javah, которую до сих пор упоминают старые руководства, объявлена устаревшей в JDK 9 и удалена в JDK 10 — если инструкция советует javah, она написана до 2018 года.
Полный пример: от Java до библиотеки
Java-класс с одним нативным методом:
public class Demo {
static {
System.loadLibrary("demo"); // libdemo.so | libdemo.dylib | demo.dll
}
private native int sum(int a, int b);
public static void main(String[] args) {
System.out.println(new Demo().sum(2, 3)); // 5
}
} После javac -h . Demo.java в файле Demo.h появится готовое объявление:
JNIEXPORT jint JNICALL Java_Demo_sum(JNIEnv *, jobject, jint, jint); Остаётся написать реализацию:
#include "Demo.h"
JNIEXPORT jint JNICALL Java_Demo_sum(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b;
} Сборка библиотеки и запуск (Linux):
gcc -shared -fPIC \
-I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" \
-o libdemo.so Demo.c
java -Djava.library.path=. Demo Обратите внимание на имена: в Java передаётся "demo", а файл называется libdemo.so. Префикс lib и расширение JVM подставляет сама, по правилам текущей платформы: libdemo.so в Linux, libdemo.dylib в macOS, demo.dll в Windows. Писать расширение в loadLibrary() не нужно — иначе библиотека не найдётся.
Правила и ограничения native-методов
Компилятор строго следит за тем, где native уместен, а где нет.
| Конструкция | Допустимо? | Комментарий |
|---|---|---|
native + тело метода | Нет | Ошибка компиляции: native methods cannot have a body. Объявление заканчивается ; |
native + abstract | Нет | Illegal combination of modifiers: оба означают «тела здесь нет», но abstract обещает реализацию в подклассе, а native — в библиотеке |
native + static | Да | Частый случай; в C-функцию вторым аргументом придёт jclass вместо jobject |
native + synchronized | Да | Монитор захватывается до входа в нативный код |
native + private / public / final | Да | Любой модификатор доступа, final тоже разрешён |
native у поля или класса | Нет | Modifier native not allowed here: модификатор применим только к методам |
native у конструктора | Нет | Конструктор нельзя объявить нативным; нативную инициализацию выносят в отдельный native-метод и вызывают из конструктора |
native в интерфейсе | Нет | Методы интерфейса либо абстрактные, либо default/static с телом на Java |
Ещё две особенности, о которых легко забыть: native-метод может объявлять throws и бросать обычные Java-исключения (нативный код возбуждает их через ThrowNew), и native-метод участвует в рефлексии как любой другой:
Method m = Object.class.getDeclaredMethod("hashCode");
System.out.println(Modifier.isNative(m.getModifiers())); // true Переопределение native-методов
Для наследования не имеет значения, где лежит реализация метода. Нативный метод можно переопределить обычным Java-методом в подклассе — и наоборот, обычный метод суперкласса можно переопределить нативным. Это не исключение из правил, а прямое следствие того, что native не входит в сигнатуру метода.
Самый наглядный пример есть в самой JDK: Object.hashCode() объявлен нативным, а String переопределяет его чистым Java-кодом.
// java.lang.Object
public native int hashCode();
// java.lang.String — переопределение обычным методом
public int hashCode() {
int h = hash;
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
// ...
}
return h;
} Проверить это можно рефлексией:
System.out.println(Modifier.isNative(
Object.class.getDeclaredMethod("hashCode").getModifiers())); // true
System.out.println(Modifier.isNative(
String.class.getDeclaredMethod("hashCode").getModifiers())); // false native в стандартной библиотеке
Нативные методы не экзотика — вы вызываете их каждый день, даже не подключая ни одной сторонней библиотеки:
Object.hashCode(),Object.getClass(),Object.clone(),Object.notifyAll();System.arraycopy(),System.currentTimeMillis(),System.nanoTime();Thread.currentThread(),Runtime.availableProcessors();- низкоуровневые методы ввода-вывода вроде
FileInputStream.read0().
Неочевидный момент
Многие такие методы помечены в исходниках JDK аннотацией @IntrinsicCandidate. Это значит, что JIT-компилятор HotSpot вправе подставить вместо вызова готовый машинный код (интринсик) прямо в точке вызова. То есть System.arraycopy() или Math.sqrt() в горячем коде обычно вообще не проходят через JNI — ключевое слово native здесь описывает лишь то, что тела на Java нет.
Чем заменить JNI: Foreign Function & Memory API
JNI требует писать и собирать C-обвязку под каждую платформу, а любая ошибка в ней роняет весь процесс JVM. Поэтому в JDK развивают безопасную замену — Foreign Function & Memory API (пакет java.lang.foreign, проект Panama). Он прошёл несколько preview-итераций в JDK 19–21 и был финализирован в JDK 22 (JEP 454).
Вызов функции strlen из стандартной библиотеки C — без единой строки на C:
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class FfmDemo {
public static void main(String[] args) throws Throwable {
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
try (Arena arena = Arena.ofConfined()) {
MemorySegment text = arena.allocateFrom("Hello, native!");
System.out.println((long) strlen.invoke(text)); // 14
}
}
} | Критерий | JNI + native | FFM API (java.lang.foreign) |
|---|---|---|
| Нужен код на C | Да, обвязка под каждый метод | Нет, вызов описывается из Java |
| Сборка | Свой .so/.dll под каждую ОС и архитектуру | Только сама целевая библиотека |
| Ошибка в нативном коде | Падение всей JVM без стектрейса Java | Часть ошибок превращается в исключения Java |
| Управление памятью | Ручное, утечки на совести разработчика | Arena детерминированно освобождает память |
| Доступность | С самых первых версий Java | Стабильный API с JDK 22 |
Что изменилось в свежих JDK
Начиная с JDK 24 (JEP 472) JVM печатает предупреждение при загрузке нативной библиотеки и при связывании native-методов: доступ к нативному коду переводят в разряд явно разрешаемых операций. Убирается это флагом запуска --enable-native-access=ALL-UNNAMED (или указанием конкретного модуля). Сам модификатор native никуда не исчезает и остаётся частью языка.
Где чаще всего спотыкаются
- Забыли загрузить библиотеку. Без
System.loadLibrary()первый же вызов даётUnsatisfiedLinkError. Загрузку почти всегда помещают в статический блок класса, чтобы она произошла один раз при инициализации. - Библиотека не в
java.library.path. Файл рядом с.classне считается:loadLibrary()ищет по системному пути. Помогает-Djava.library.path=.илиSystem.load()с абсолютным путём. - Переименовали класс или пакет. Имя C-функции завязано на полное имя класса, поэтому любой рефакторинг требует перегенерации заголовка через
javac -hи пересборки библиотеки. - Забыли
extern "C"в C++. Компилятор C++ искажает имена функций (name mangling), и JVM не находит нужный символ. - Разрядность не совпала. Классическое Can't load IA 32-bit .dll on a AMD 64-bit platform: 32-битная библиотека не грузится в 64-битную JVM.
- Ошибка в нативном коде роняет процесс. Разыменование нулевого указателя в C не превращается в
NullPointerException: JVM завершается по SIGSEGV и оставляет файлhs_err_pid<N>.log. Такое падение нельзя перехватитьtry/catch. - Программа перестала быть кроссплатформенной. Один и тот же jar с JNI требует отдельной сборки библиотеки под каждую пару «ОС + архитектура» — принцип «написано один раз, работает везде» действует только для байт-кода.
Итоги
nativeобъявляет метод, реализация которого лежит вне JVM, в скомпилированной библиотеке; тела у такого метода нет.- Связывание выполняет JNI: библиотеку грузит
System.loadLibrary(), имя C-функции строится какJava_пакет_Класс_метод, заголовок генерируетjavac -h. nativeнельзя сочетать сabstract, нельзя применять к полям, конструкторам и методам интерфейсов;static,final,synchronizedи любые модификаторы доступа разрешены.- Нативный метод переопределяется обычным Java-методом в подклассе — как
Object.hashCode()вString. - Для нового кода стоит смотреть в сторону FFM API из
java.lang.foreign(стабилен с JDK 22): он решает те же задачи без C-обвязки и с управляемым временем жизни памяти.
Часто задаваемые вопросы
Почему возникает UnsatisfiedLinkError, если библиотека лежит рядом с классом?
Метод System.loadLibrary() ищет файл не в каталоге класса, а в системном пути java.library.path. Запустите программу с флагом -Djava.library.path=. либо используйте System.load() с абсолютным путём. Если библиотека загрузилась, но ошибка осталась, значит не совпало имя символа: проверьте, что C-функция называется Java_пакет_Класс_метод, что в C++ она объявлена внутри extern "C" и что разрядность библиотеки совпадает с разрядностью JVM.
На каких языках, кроме C и C++, можно написать реализацию native-метода?
На любом, который умеет собираться в динамическую библиотеку и экспортировать функцию с C-соглашением о вызове: Rust, Go (через cgo), Fortran, Objective-C, ассемблер. JNI не привязан к конкретному языку — ему нужен экспортированный символ с правильным именем и сигнатурой, начинающейся с указателя JNIEnv*.
Можно ли пометить native конструктор, поле или метод интерфейса?
Нет. Модификатор native применим только к методам класса. Для конструктора компилятор выдаст modifier native not allowed here; нативную инициализацию выносят в отдельный нативный метод и вызывают его из конструктора. В интерфейсе метод может быть только абстрактным либо default/static с телом на Java. Также native несовместим с abstract.
Нужно ли переписывать существующий JNI-код на FFM API?
Срочной необходимости нет: JNI поддерживается и работает, а модификатор native остаётся частью языка. Но для нового кода на JDK 22 и выше FFM API из пакета java.lang.foreign удобнее: не нужна C-обвязка и отдельная сборка под каждую платформу, память освобождается детерминированно через Arena. Учтите, что с JDK 24 и JNI, и FFM считаются ограниченными операциями и печатают предупреждение без флага --enable-native-access.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии