OOP Basics ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-08-08

The native Keyword in Java

The code below compiles without a single warning. It dies on the first call:

public class Demo {
    private native int sum(int a, int b);

    public static void main(String[] args) {
        System.out.println(new Demo().sum(2, 3));
    }
}
// Exception in thread "main" java.lang.UnsatisfiedLinkError: 'int Demo.sum(int, int)'

That is the whole personality of native in one screen: the compiler takes your word that an implementation exists somewhere outside the JVM, and nobody checks that claim until the method is actually invoked.

1. What the native keyword does

native is a Java method modifier that says the body of the method is implemented outside the JVM — in a compiled library (.dll, .so, .dylib) written in a language with a C-compatible ABI. The Java side carries only the signature: there is no body, and the declaration ends with a semicolon.

public native int hashCode();

The keyword itself compiles nothing and links nothing. It does exactly one job: it allows a method to be declared without a body and marks it as "the implementation arrives from elsewhere". Connecting that declaration to a real function is the job of JNI (Java Native Interface) at runtime.

Why anyone reaches for it in practice:

  • operating-system features the standard library does not expose — device access, platform-specific system calls;
  • reusing mature C and C++ libraries that nobody wants to rewrite: codecs, cryptography, drivers, machine-learning engines;
  • performance-critical spots that need hand-written SIMD code or manual memory control;
  • the internals of the JDK itself — parts of Object, System and Thread simply cannot be expressed in Java.

Important

native does not mean faster. Every crossing from Java into native code and back has a price: the JVM has to fix up thread state, the JIT loses optimizations such as inlining across the call, and objects have to be reached through JNI functions. A small method called in a tight loop is frequently slower after being moved to C than the plain Java version it replaced.

2. JNI: how the JVM finds the implementation

JNI is the standard interface between the JVM and native code. For a call to a native method to succeed, three things have to line up.

  1. The library is loaded. This normally happens in a static initializer: System.loadLibrary("demo") searches java.library.path, while System.load("/absolute/path/libdemo.so") takes a full path.
  2. The function name matches. The JVM looks up a symbol built by a strict rule: Java_ + the package with dots replaced by underscores + _ + the class name + _ + the method name. For com.example.Demo.sum() that is Java_com_example_Demo_sum.
  3. The signature matches. The first parameter of the C function is always JNIEnv*; the second is jobject for an instance method or jclass for a static one; then come the actual parameters in JNI types (jint, jlong, jstring, jobjectArray …).

You never have to spell those names out by hand — the compiler generates the header for you:

javac -h . Demo.java

The -h flag was added to javac in JDK 8. The separate javah tool that older tutorials still recommend was deprecated in JDK 9 and removed in JDK 10: if a guide tells you to run javah, it was written before 2018.

3. A complete example: from Java to a shared library

A Java class with one native method:

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
    }
}

After javac -h . Demo.java, the generated Demo.h already contains the declaration you have to implement:

JNIEXPORT jint JNICALL Java_Demo_sum(JNIEnv *, jobject, jint, jint);

The implementation itself is ordinary C:

#include "Demo.h"

JNIEXPORT jint JNICALL Java_Demo_sum(JNIEnv *env, jobject obj, jint a, jint b) {
    return a + b;
}

Building the library and running the program on Linux:

gcc -shared -fPIC \
    -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" \
    -o libdemo.so Demo.c

java -Djava.library.path=. Demo

Watch the naming: Java passes "demo", yet the file on disk is called libdemo.so. The JVM adds the platform-specific prefix and extension itself — libdemo.so on Linux, libdemo.dylib on macOS, demo.dll on Windows. Writing the extension inside loadLibrary() is a mistake: the library will not be found.

4. Rules and restrictions of native methods

The compiler is strict about where native is accepted and where it is not.

Declaration Legal? Details
native with a method body No Compile-time error native methods cannot have a body. The declaration ends with ;
native + abstract No Illegal combination of modifiers. Both mean "no body here", but abstract promises an implementation in a subclass and native promises one in a library
native + static Yes Very common. The C function then receives jclass as its second argument instead of jobject
native + synchronized Yes The monitor is acquired before control enters the native code
native + private / public / final Yes Any access modifier is allowed, and so is final
native on a field or a class No Modifier native not allowed here: the keyword applies to methods only
native on a constructor No A constructor cannot be native. Move the native initialization into a separate native method and call it from the constructor
native in an interface No Interface methods are either abstract or default/static with a Java body

Two more details that are easy to forget. A native method may declare throws and throw ordinary Java exceptions — the native side raises them through the JNI function ThrowNew. And a native method takes part in reflection like any other method:

Method m = Object.class.getDeclaredMethod("hashCode");
System.out.println(Modifier.isNative(m.getModifiers())); // true

5. Overriding native methods

Inheritance does not care where the implementation lives. A native method can be overridden by an ordinary Java method in a subclass, and an ordinary method can just as well be overridden by a native one. This is not a special case: native is not part of the method signature, so it plays no role in overriding at all.

The clearest example ships with the JDK. Object.hashCode() is declared native, while String overrides it with pure Java code:

// java.lang.Object
public native int hashCode();

// java.lang.String - overridden by a regular Java method
public int hashCode() {
    int h = hash;
    if (h == 0 && !hashIsZero) {
        h = isLatin1() ? StringLatin1.hashCode(value)
                       : StringUTF16.hashCode(value);
        // ...
    }
    return h;
}

Reflection confirms it in two lines:

System.out.println(Modifier.isNative(
        Object.class.getDeclaredMethod("hashCode").getModifiers())); // true
System.out.println(Modifier.isNative(
        String.class.getDeclaredMethod("hashCode").getModifiers())); // false

6. native methods in the standard library

Native methods are not an exotic corner of the language — you call them every day without adding a single third-party dependency:

  • Object.hashCode(), Object.getClass(), Object.clone(), Object.notifyAll();
  • System.arraycopy(), System.currentTimeMillis(), System.nanoTime();
  • Thread.currentThread(), Runtime.availableProcessors();
  • low-level I/O methods such as FileInputStream.read0().

Worth knowing

Many of those methods are annotated @IntrinsicCandidate in the JDK sources. That allows the HotSpot JIT to replace the call with hand-written machine code (an intrinsic) right at the call site. So in hot code System.arraycopy() or Math.sqrt() never travel the JNI path at all — here native only records the fact that there is no Java body, and says nothing about how the call is executed.

7. Replacing JNI: the Foreign Function & Memory API

JNI makes you write and build a C shim for every platform you support, and a single mistake in that shim takes down the whole JVM process. That is why the JDK now ships a safer alternative: the Foreign Function & Memory API in the java.lang.foreign package (Project Panama). It went through several preview rounds in JDK 19–21 and was finalized in JDK 22 (JEP 454).

Calling strlen from the C standard library, without writing one line of 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
        }
    }
}
Criterion JNI + native FFM API (java.lang.foreign)
C code required Yes, a shim for every method No, the call is described from Java
Build Your own .so/.dll per OS and architecture Only the target library itself
A bug in the native code The whole JVM crashes, no Java stack trace Many failures surface as Java exceptions
Memory management Manual; leaks are on you Arena frees memory deterministically
Availability Since the very first Java releases Stable API since JDK 22

What changed in recent JDKs

Starting with JDK 24 (JEP 472) the JVM prints a warning when a native library is loaded and when native methods are bound: reaching native code is becoming an operation you have to allow explicitly. Silence it with the launcher flag --enable-native-access=ALL-UNNAMED, or name the specific module. The native modifier itself is not going anywhere — it stays part of the language.

8. Errors you will actually hit

  • The library was never loaded. Without System.loadLibrary() the first call throws UnsatisfiedLinkError. Put the call in a static initializer block so it runs once, when the class is initialized.
  • The library is not on java.library.path. Sitting next to the .class file counts for nothing: loadLibrary() searches the system path. Use -Djava.library.path=. or System.load() with an absolute path.
  • The class or package was renamed. The C function name is derived from the fully qualified class name, so any refactoring means regenerating the header with javac -h and rebuilding the library.
  • Missing extern "C" in C++. The C++ compiler mangles function names, and the JVM then fails to find the symbol it is looking for.
  • Architecture mismatch. The classic Can't load IA 32-bit .dll on a AMD 64-bit platform: a 32-bit library will not load into a 64-bit JVM.
  • A bug in native code kills the process. Dereferencing a null pointer in C does not become a NullPointerException: the JVM dies on SIGSEGV and leaves an hs_err_pid<N>.log file behind. No try/catch can intercept that.
  • The application stops being portable. One jar with JNI still needs a separate library build for every OS and architecture pair — "write once, run anywhere" applies to bytecode, not to your .so files.

9. Key takeaways

  • native declares a method whose implementation lives outside the JVM, in a compiled library; such a method has no body in Java.
  • JNI performs the binding: System.loadLibrary() loads the library, the C function is named Java_package_Class_method, and the header is generated by javac -h (javah was removed in JDK 10).
  • native cannot be combined with abstract and cannot be applied to fields, constructors or interface methods; static, final, synchronized and any access modifier are fine.
  • A native method can be overridden by a regular Java method — exactly what String does with Object.hashCode().
  • For new code, look at the FFM API in java.lang.foreign (stable since JDK 22): it solves the same problems without a C shim and with a deterministic memory lifetime.

Frequently asked questions

Why do I get UnsatisfiedLinkError when the library sits right next to my class file?

System.loadLibrary() does not look in the directory of the class — it searches the system property java.library.path. Run the program with -Djava.library.path=. or switch to System.load() with an absolute path. If the library loads but the error persists, the symbol name does not match: check that the C function is called Java_package_Class_method, that in C++ it is declared inside extern "C", and that the library and the JVM have the same architecture.

Can a native method be implemented in something other than C or C++?

Yes. Any language that can be compiled into a dynamic library and export a function with the C calling convention will do: Rust, Go through cgo, Fortran, Objective-C, even assembly. JNI is not tied to a particular language — it needs an exported symbol with the right name and a signature that starts with a JNIEnv pointer.

Can a constructor, a field or an interface method be native?

No. The native modifier applies to class methods only. On a constructor the compiler reports modifier native not allowed here; the usual workaround is a separate native method called from the constructor. An interface method is either abstract or a default/static method with a Java body. And native can never be combined with abstract.

The tutorial tells me to run javah, but the command is missing. What do I use instead?

javah was deprecated in JDK 9 and removed in JDK 10. The compiler generates JNI headers itself: run javac -h <output-dir> MyClass.java, available since JDK 8. It produces the same .h file with the JNIEXPORT declarations, so no separate tool is needed in the build.

Should I rewrite existing JNI code with the FFM API?

There is no urgency: JNI is supported and works, and the native modifier remains part of the language. For new code on JDK 22 and later the FFM API in java.lang.foreign is more convenient — no C shim, no per-platform build of your own, and memory released deterministically through Arena. Keep in mind that since JDK 24 both JNI and FFM count as restricted operations and print a warning unless you pass --enable-native-access.

Comments

Please log in or register to have a possibility to add comment.