ООП ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-08-08

Модификатор 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-метода сработал, должны совпасть три вещи.

  1. Библиотека загружена. Обычно это делают в статическом инициализаторе класса: System.loadLibrary("demo") (ищет библиотеку в java.library.path) или System.load("/абсолютный/путь/libdemo.so").
  2. Имя функции совпадает. JVM ищет символ по строгому соглашению: Java_ + пакет с точками, заменёнными на подчёркивания + _ + имя класса + _ + имя метода. Для com.example.Demo.sum() это Java_com_example_Demo_sum.
  3. Сигнатура совпадает. Первым параметром 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.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.