This walks you from adding the dependency to your first compress/decompress round-trip.
zstd-java is on Maven Central. The zstd jar is pure Java and ships no libzstd
itself — you also need a native artifact. Simplest: depend on zstd-platform,
which bundles the bindings plus every platform's native library, so the build
runs on any OS/arch:
<dependency>
<groupId>io.github.dfa1.zstd</groupId>
<artifactId>zstd-platform</artifactId>
<version>0.4</version>
</dependency>implementation("io.github.dfa1.zstd:zstd-platform:0.4")That pulls all six natives (~3.8 MB, five unused per platform). When you target a known platform and want only its native, import the BOM to pin versions and pick the matching classifier:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.dfa1.zstd</groupId>
<artifactId>zstd-bom</artifactId>
<version>0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.dfa1.zstd</groupId>
<artifactId>zstd</artifactId>
</dependency>
<dependency>
<groupId>io.github.dfa1.zstd</groupId>
<artifactId>zstd-native-osx-aarch64</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>implementation(platform("io.github.dfa1.zstd:zstd-bom:0.4"))
implementation("io.github.dfa1.zstd:zstd")
runtimeOnly("io.github.dfa1.zstd:zstd-native-osx-aarch64")Classifiers: osx-aarch64, osx-x86_64, linux-x86_64, linux-aarch64,
windows-x86_64, windows-aarch64. Need JDK 25+. Building the native lib from
source is only for contributors — see the reference.
Every artifact has zero transitive dependencies — the bindings are the JDK's
FFM API plus the bundled libzstd, nothing else on your classpath.
import io.github.dfa1.zstd.Zstd;
byte[] original = "hello world".getBytes();
byte[] packed = Zstd.compress(original);
byte[] restored = Zstd.decompress(packed); // size read from the frame header
assert java.util.Arrays.equals(original, restored);The FFM API requires an explicit flag:
java --enable-native-access=ALL-UNNAMED Demo.javaThe byte[] round-trip above copies your data onto the heap on the way in and
off it on the way out. When your bytes are already off-heap — an mmap slice,
an arena buffer — that copy is pure waste. This is the library's strong point:
the MemorySegment overloads hand zstd the segment address directly, so there is
no copy in, no copy out, and no per-call heap allocation (hence no GC churn).
try (Arena arena = Arena.ofConfined();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
MemorySegment frame = reader.mmapSlice(); // already native — never touches the heap
long n = Zstd.decompressedSize(frame); // read the header, no copy
MemorySegment out = arena.allocate(n); // this segment *is* the output buffer
dctx.decompress(out, frame); // native → native
}In benchmarks this path allocates ~0 bytes/op regardless of payload size, while
the byte[] path allocates the full output every call. See
docs/benchmarks.md for numbers and docs/zero-copy.md
for when it pays.
For large or unbounded data, don't buffer the whole thing — wrap an ordinary
java.io stream. ZstdOutputStream / ZstdInputStream compress and decompress
incrementally, so memory stays flat no matter how big the payload.
// compress a file as you write it
try (var out = new ZstdOutputStream(Files.newOutputStream(packed), 9)) {
Files.copy(source, out);
}
// decompress as you read it back (transferTo loops internally until EOF)
try (var in = new ZstdInputStream(Files.newInputStream(packed));
var out = Files.newOutputStream(restored)) {
in.transferTo(out);
}They are plain OutputStream / InputStream subclasses — drop them into any code
that already speaks java.io.
That's the whole loop. From here, pick a how-to guide for your actual task, or browse the reference.