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,SystemandThreadsimply 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.
- The library is loaded. This normally happens in a static initializer:
System.loadLibrary("demo")searchesjava.library.path, whileSystem.load("/absolute/path/libdemo.so")takes a full path. - 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. Forcom.example.Demo.sum()that isJava_com_example_Demo_sum. - The signature matches. The first parameter of the C function is always
JNIEnv*; the second isjobjectfor an instance method orjclassfor astaticone; 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 throwsUnsatisfiedLinkError. 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.classfile counts for nothing:loadLibrary()searches the system path. Use-Djava.library.path=.orSystem.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 -hand 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 anhs_err_pid<N>.logfile behind. Notry/catchcan 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
.sofiles.
9. Key takeaways
nativedeclares 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 namedJava_package_Class_method, and the header is generated byjavac -h(javahwas removed in JDK 10). nativecannot be combined withabstractand cannot be applied to fields, constructors or interface methods;static,final,synchronizedand any access modifier are fine.- A native method can be overridden by a regular Java method — exactly what
Stringdoes withObject.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